Initial commit
Some checks failed
CI / build (push) Has been cancelled

This commit is contained in:
2026-06-11 11:03:01 +02:00
commit d1ff0e30ba
138 changed files with 5658 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
plugins {
alias(libs.plugins.architecture.android.library)
alias(libs.plugins.architecture.compose)
}
android {
namespace = "com.example.architecture.core.design.system"
}
dependencies {
// Coil is internal to NetworkImage; no Coil types leak into public signatures.
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
}

View File

@@ -0,0 +1,58 @@
package com.example.architecture.core.design.system.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.architecture.core.design.system.theme.AppTheme
/**
* Slot-API card. Callers compose into an optional [header] slot and the [content] slot
* (a `ColumnScope`), and may make the whole card clickable. Feature code decides what goes inside.
*/
@Composable
fun AppCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
header: (@Composable () -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
val body: @Composable ColumnScope.() -> Unit = {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
header?.invoke()
content()
}
}
if (onClick != null) {
Card(onClick = onClick, modifier = modifier, content = body)
} else {
Card(modifier = modifier, content = body)
}
}
@Preview
@Composable
private fun AppCardPreview() {
AppTheme {
AppCard(
modifier = Modifier.padding(16.dp),
onClick = {},
header = { Text("Rick Sanchez", style = MaterialTheme.typography.titleMedium) },
) {
Text(
text = "Human · Alive · Earth (C-137)",
style = MaterialTheme.typography.bodyMedium,
)
}
}
}

View File

@@ -0,0 +1,23 @@
package com.example.architecture.core.design.system.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
* Thin wrapper over [Scaffold] giving screens a consistent surface. Slot API: callers provide the
* [topBar] and the [content] (which receives the inner [PaddingValues] to consume).
*/
@Composable
fun AppScaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = topBar,
content = content,
)
}

View File

@@ -0,0 +1,56 @@
package com.example.architecture.core.design.system.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.architecture.core.design.system.R
import com.example.architecture.core.design.system.theme.AppTheme
/**
* Centered error message with an optional retry button. The message is already-resolved text
* (the caller maps its error/`UiText` to a String); the retry label is localized here.
*/
@Composable
fun ErrorState(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
)
if (onRetry != null) {
Button(onClick = onRetry) {
Text(text = stringResource(R.string.designsystem_retry))
}
}
}
}
@Preview
@Composable
private fun ErrorStatePreview() {
AppTheme {
ErrorState(message = "No internet connection.", onRetry = {})
}
}

View File

@@ -0,0 +1,23 @@
package com.example.architecture.core.design.system.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.architecture.core.design.system.theme.AppTheme
@Composable
fun LoadingIndicator(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
@Preview
@Composable
private fun LoadingIndicatorPreview() {
AppTheme { LoadingIndicator() }
}

View File

@@ -0,0 +1,31 @@
package com.example.architecture.core.design.system.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
/**
* Coil-backed remote image. Coil 3 auto-registers the OkHttp network fetcher from
* `coil-network-okhttp` on the classpath, so callers just pass a URL.
*/
@Composable
fun NetworkImage(
imageUrl: String?,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop,
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.build(),
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier,
)
}

View File

@@ -0,0 +1,49 @@
package com.example.architecture.core.design.system.modifier
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme
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.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
/**
* Animated placeholder shimmer for loading skeletons. Implemented as a `Modifier` extension (not a
* `@Composable`); `composed` lets it read the theme and animate while the gradient is repainted
* below the recomposition layer via [background].
*/
fun Modifier.shimmerEffect(): Modifier = composed {
var size by remember { mutableStateOf(IntSize.Zero) }
val transition = rememberInfiniteTransition(label = "shimmer")
val startOffsetX by transition.animateFloat(
initialValue = -2f * size.width,
targetValue = 2f * size.width,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200),
repeatMode = RepeatMode.Restart,
),
label = "shimmerOffsetX",
)
val base = MaterialTheme.colorScheme.surfaceVariant
val highlight = MaterialTheme.colorScheme.surface
background(
brush = Brush.linearGradient(
colors = listOf(base, highlight, base),
start = Offset(startOffsetX, 0f),
end = Offset(startOffsetX + size.width, size.height.toFloat()),
),
).onGloballyPositioned { size = it.size }
}

View File

@@ -0,0 +1,45 @@
package com.example.architecture.core.design.system.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
// Brand palette - seeded from the Android green used by the project.
private val Green10 = Color(0xFF00210B)
private val Green20 = Color(0xFF003918)
private val Green40 = Color(0xFF1E6C36)
private val Green80 = Color(0xFF8FD89B)
private val Green90 = Color(0xFFAAF5B5)
private val Teal40 = Color(0xFF36687A)
private val Teal80 = Color(0xFF9ECEE3)
private val Neutral10 = Color(0xFF191C1A)
private val Neutral90 = Color(0xFFE1E3DE)
private val Neutral99 = Color(0xFFFBFDF7)
internal val LightColorScheme = lightColorScheme(
primary = Green40,
onPrimary = Color.White,
primaryContainer = Green90,
onPrimaryContainer = Green10,
secondary = Teal40,
onSecondary = Color.White,
background = Neutral99,
onBackground = Neutral10,
surface = Neutral99,
onSurface = Neutral10,
)
internal val DarkColorScheme = darkColorScheme(
primary = Green80,
onPrimary = Green20,
primaryContainer = Green40,
onPrimaryContainer = Green90,
secondary = Teal80,
onSecondary = Neutral10,
background = Neutral10,
onBackground = Neutral90,
surface = Neutral10,
onSurface = Neutral90,
)

View File

@@ -0,0 +1,11 @@
package com.example.architecture.core.design.system.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
internal val AppShapes = Shapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
)

View File

@@ -0,0 +1,22 @@
package com.example.architecture.core.design.system.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
/**
* The single Compose theme for the app. Every screen and every `@Preview` is wrapped in this so
* they reflect real appearance. Dynamic color is intentionally off to keep the brand identity.
*/
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content,
)
}

View File

@@ -0,0 +1,6 @@
package com.example.architecture.core.design.system.theme
import androidx.compose.material3.Typography
// Material3 baseline type scale. Swap in custom font families here if the brand needs them.
internal val AppTypography = Typography()

View File

@@ -0,0 +1,3 @@
<resources>
<string name="designsystem_retry">Retry</string>
</resources>