diff --git a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/KSActivity.kt b/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/KSActivity.kt
index 2bc661c1..16b35cff 100644
--- a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/KSActivity.kt
+++ b/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/KSActivity.kt
@@ -5,15 +5,14 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import dagger.hilt.android.AndroidEntryPoint
-import io.github.reactivecircus.kstreamlined.android.theme.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.feature.home.HomeScreen
@AndroidEntryPoint
class KSActivity : ComponentActivity() {
@@ -28,15 +27,12 @@ class KSActivity : ComponentActivity() {
setContent {
KSTheme {
val darkTheme = isSystemInDarkTheme()
- val navigationBarColor = MaterialTheme.colorScheme.background.toArgb()
+ val navigationBarColor = KSTheme.colorScheme.background.toArgb()
LaunchedEffect(darkTheme) {
window.navigationBarColor = navigationBarColor
}
- Surface(
- modifier = Modifier.fillMaxSize(),
- ) {
- }
+ HomeScreen(modifier = Modifier.navigationBarsPadding())
}
}
}
diff --git a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/di/AppModule.kt b/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/di/AppModule.kt
index 6e4dec6d..5d8621f0 100644
--- a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/di/AppModule.kt
+++ b/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/di/AppModule.kt
@@ -19,7 +19,6 @@ object AppModule {
fun imageLoader(@ApplicationContext context: Context): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(enable = true)
- // TODO default drawables for fallback, error, placeholder
// only enable hardware bitmaps on API 28+. See: https://github.com/coil-kt/coil/issues/159
.allowHardware(enable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
.build()
diff --git a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/theme/Theme.kt b/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/theme/Theme.kt
deleted file mode 100644
index 30d16a7f..00000000
--- a/android/app/src/main/java/io/github/reactivecircus/kstreamlined/android/theme/Theme.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package io.github.reactivecircus.kstreamlined.android.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-
-// TODO define color scheme
-
-private val DarkColorScheme = darkColorScheme(
- primary = Color(0xFF7F52FF),
- background = Color(0xFF070426),
- surface = Color(0xFF070426),
-)
-
-private val LightColorScheme = lightColorScheme(
- primary = Color(0xFF7F52FF),
- background = Color(0xFFE7DBFF),
- surface = Color(0xFFE7DBFF),
-)
-
-@Composable
-fun KSTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- content: @Composable () -> Unit
-) {
- val colorScheme = if (darkTheme) {
- DarkColorScheme
- } else {
- LightColorScheme
- }
-
- // TODO customize shapes and typography
- MaterialTheme(
- colorScheme = colorScheme,
- content = content,
- )
-}
diff --git a/android/common-ui/feed/build.gradle.kts b/android/common-ui/feed/build.gradle.kts
new file mode 100644
index 00000000..2cd6cd49
--- /dev/null
+++ b/android/common-ui/feed/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+ id("kstreamlined.android.library")
+ id("kstreamlined.android.library.compose")
+}
+
+android {
+ namespace = "io.github.reactivecircus.kstreamlined.android.common.ui.feed"
+ buildFeatures {
+ androidResources = true
+ }
+}
+
+androidComponents {
+ beforeVariants {
+ @Suppress("UnstableApiUsage")
+ it.androidTest.enable = false
+ }
+}
+
+dependencies {
+ implementation(project(":designsystem"))
+ implementation(project(":kmp:model"))
+
+ // Compose
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.ui.tooling)
+}
diff --git a/android/common-ui/feed/src/main/AndroidManifest.xml b/android/common-ui/feed/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8072ee00
--- /dev/null
+++ b/android/common-ui/feed/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/android/common-ui/feed/src/main/java/io/github/reactivecircus/kstreamlined/android/common/ui/feed/KotlinWeeklyCard.kt b/android/common-ui/feed/src/main/java/io/github/reactivecircus/kstreamlined/android/common/ui/feed/KotlinWeeklyCard.kt
new file mode 100644
index 00000000..851fb539
--- /dev/null
+++ b/android/common-ui/feed/src/main/java/io/github/reactivecircus/kstreamlined/android/common/ui/feed/KotlinWeeklyCard.kt
@@ -0,0 +1,114 @@
+package io.github.reactivecircus.kstreamlined.android.common.ui.feed
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.github.reactivecircus.kstreamlined.android.designsystem.ThemePreviews
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.IconButton
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Surface
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Text
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.BookmarkAdd
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.BookmarkFill
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
+
+@Composable
+public fun KotlinWeeklyCard(
+ item: FeedItem.KotlinWeekly,
+ onItemClick: (FeedItem.KotlinWeekly) -> Unit,
+ onSaveButtonClick: (FeedItem.KotlinWeekly) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Surface(
+ onClick = { onItemClick(item) },
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.primary,
+ contentColor = KSTheme.colorScheme.onPrimary,
+ elevation = 4.dp,
+ ) {
+ Row(
+ modifier = Modifier.padding(vertical = 24.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = item.title,
+ style = KSTheme.typography.titleLarge,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = item.publishTime,
+ style = KSTheme.typography.bodyMedium,
+ )
+ }
+ IconButton(
+ if (item.savedForLater) {
+ KSIcons.BookmarkFill
+ } else {
+ KSIcons.BookmarkAdd
+ },
+ contentDescription = null,
+ onClick = { onSaveButtonClick(item) },
+ modifier = Modifier.padding(end = 8.dp),
+ )
+ }
+ }
+}
+
+@Composable
+@ThemePreviews
+private fun PreviewKotlinWeeklyCard_unsaved() {
+ KSTheme {
+ Surface {
+ KotlinWeeklyCard(
+ item = FeedItem.KotlinWeekly(
+ id = "1",
+ title = "Kotlin Weekly #381",
+ publishTime = "Moments ago",
+ contentUrl = "contentUrl",
+ savedForLater = false,
+ ),
+ onItemClick = {},
+ onSaveButtonClick = {},
+ modifier = Modifier.padding(24.dp),
+ )
+ }
+ }
+}
+
+@Composable
+@ThemePreviews
+private fun PreviewKotlinWeeklyCard_saved() {
+ KSTheme {
+ Surface {
+ KotlinWeeklyCard(
+ item = FeedItem.KotlinWeekly(
+ id = "1",
+ title = "Kotlin Weekly #381",
+ publishTime = "3 hours ago",
+ contentUrl = "contentUrl",
+ savedForLater = true,
+ ),
+ onItemClick = {},
+ onSaveButtonClick = {},
+ modifier = Modifier.padding(24.dp),
+ )
+ }
+ }
+}
diff --git a/android/designsystem/build.gradle.kts b/android/designsystem/build.gradle.kts
index 34d9ff82..7f4a28e7 100644
--- a/android/designsystem/build.gradle.kts
+++ b/android/designsystem/build.gradle.kts
@@ -14,6 +14,5 @@ dependencies {
// Compose
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.foundation)
- debugImplementation(libs.androidx.compose.ui.tooling)
- implementation(libs.androidx.compose.ui.toolingPreview)
+ implementation(libs.androidx.compose.ui.tooling)
}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/ThemePreviews.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/ThemePreviews.kt
new file mode 100644
index 00000000..de46c0d8
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/ThemePreviews.kt
@@ -0,0 +1,8 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem
+
+import android.content.res.Configuration
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark")
+public annotation class ThemePreviews
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Divider.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Divider.kt
new file mode 100644
index 00000000..0a6dcaf0
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Divider.kt
@@ -0,0 +1,39 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import androidx.compose.material3.HorizontalDivider as MaterialHorizontalDivider
+import androidx.compose.material3.VerticalDivider as MaterialVerticalDivider
+
+@Composable
+@NonRestartableComposable
+public fun HorizontalDivider(
+ modifier: Modifier = Modifier,
+ thickness: Dp = 1.dp,
+ color: Color = KSTheme.colorScheme.outline,
+) {
+ MaterialHorizontalDivider(
+ modifier = modifier,
+ thickness = thickness,
+ color = color,
+ )
+}
+
+@Composable
+@NonRestartableComposable
+public fun VerticalDivider(
+ modifier: Modifier = Modifier,
+ thickness: Dp = 1.dp,
+ color: Color = KSTheme.colorScheme.outline,
+) {
+ MaterialVerticalDivider(
+ modifier = modifier,
+ thickness = thickness,
+ color = color,
+ )
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Icon.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Icon.kt
new file mode 100644
index 00000000..c9573bd4
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Icon.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 Woolworths. All rights reserved.
+ */
+
+package io.github.reactivecircus.kstreamlined.android.designsystem.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalContentColor
+import androidx.compose.material3.Icon as MaterialIcon
+
+@Composable
+@NonRestartableComposable
+public fun Icon(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+) {
+ MaterialIcon(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ tint = tint,
+ )
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/IconButton.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/IconButton.kt
new file mode 100644
index 00000000..8de45fcf
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/IconButton.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2022 Woolworths. All rights reserved.
+ */
+
+package io.github.reactivecircus.kstreamlined.android.designsystem.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalContentColor
+
+@Composable
+@NonRestartableComposable
+public fun IconButton(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ iconTint: Color = LocalContentColor.current,
+) {
+ IconButtonImpl(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ iconTint = iconTint,
+ containerSize = DefaultContainerSize,
+ iconSize = DefaultIconSize,
+ )
+}
+
+@Composable
+@NonRestartableComposable
+public fun LargeIconButton(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ iconTint: Color = LocalContentColor.current,
+) {
+ IconButtonImpl(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ iconTint = iconTint,
+ containerSize = LargeContainerSize,
+ iconSize = LargeIconSize,
+ )
+}
+
+@Composable
+private fun IconButtonImpl(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean,
+ iconTint: Color,
+ containerSize: Dp,
+ iconSize: Dp,
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ Box(
+ modifier = modifier
+ .minimumInteractiveComponentSize()
+ .size(containerSize)
+ .clip(CircleShape)
+ .clickable(
+ onClick = onClick,
+ enabled = enabled,
+ role = Role.Button,
+ interactionSource = interactionSource,
+ indication = rememberRipple(
+ bounded = false,
+ radius = containerSize / 2
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ modifier = Modifier.requiredSize(iconSize),
+ tint = iconTint,
+ )
+ }
+}
+
+@Composable
+public fun FilledIconButton(
+ imageVector: ImageVector,
+ contentDescription: String?,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ containerColor: Color = KSTheme.colorScheme.container,
+ iconTint: Color = LocalContentColor.current,
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ Surface(
+ onClick = onClick,
+ modifier = modifier.semantics { role = Role.Button },
+ enabled = enabled,
+ shape = CircleShape,
+ color = containerColor,
+ contentColor = iconTint,
+ interactionSource = interactionSource,
+ ) {
+ Box(
+ modifier = Modifier.size(DefaultContainerSize),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ modifier = Modifier.requiredSize(DefaultIconSize),
+ tint = iconTint,
+ )
+ }
+ }
+}
+
+private val DefaultIconSize = 24.dp
+private val DefaultContainerSize = 40.dp
+
+private val LargeIconSize = 32.dp
+private val LargeContainerSize = 48.dp
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Surface.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Surface.kt
new file mode 100644
index 00000000..b7c334ff
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Surface.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 Woolworths. All rights reserved.
+ */
+
+package io.github.reactivecircus.kstreamlined.android.designsystem.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalContentColor
+import androidx.compose.material3.Surface as MaterialSurface
+
+@Composable
+@NonRestartableComposable
+public fun Surface(
+ modifier: Modifier = Modifier,
+ shape: Shape = RectangleShape,
+ color: Color = KSTheme.colorScheme.background,
+ contentColor: Color = KSTheme.colorScheme.onBackground,
+ border: BorderStroke? = null,
+ elevation: Dp = 0.dp,
+ content: @Composable () -> Unit
+) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ MaterialSurface(
+ modifier = modifier,
+ shape = shape,
+ color = color,
+ contentColor = contentColor,
+ tonalElevation = elevation,
+ shadowElevation = elevation,
+ border = border,
+ content = content,
+ )
+ }
+}
+
+@Composable
+@NonRestartableComposable
+public fun Surface(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = RectangleShape,
+ color: Color = KSTheme.colorScheme.background,
+ contentColor: Color = KSTheme.colorScheme.onBackground,
+ elevation: Dp = 0.dp,
+ border: BorderStroke? = null,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit
+) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ MaterialSurface(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ shape = shape,
+ color = color,
+ contentColor = contentColor,
+ tonalElevation = elevation,
+ shadowElevation = elevation,
+ border = border,
+ interactionSource = interactionSource,
+ content = content,
+ )
+ }
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Text.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Text.kt
new file mode 100644
index 00000000..77568293
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/component/Text.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2022 Woolworths. All rights reserved.
+ */
+
+package io.github.reactivecircus.kstreamlined.android.designsystem.component
+
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalContentColor
+
+@Composable
+public fun Text(
+ text: String,
+ style: TextStyle,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = 1,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+) {
+ val textColor = color.takeOrElse {
+ style.color.takeOrElse {
+ LocalContentColor.current
+ }
+ }
+ val mergedStyle = style.merge(
+ TextStyle(
+ color = textColor,
+ textAlign = textAlign ?: style.textAlign,
+ textDecoration = textDecoration,
+ )
+ )
+ BasicText(
+ text,
+ modifier,
+ mergedStyle,
+ onTextLayout,
+ overflow,
+ softWrap,
+ maxLines,
+ minLines,
+ )
+}
+
+@Composable
+public fun Text(
+ text: AnnotatedString,
+ style: TextStyle,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = 1,
+ inlineContent: Map = mapOf(),
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+) {
+ val textColor = color.takeOrElse {
+ style.color.takeOrElse {
+ LocalContentColor.current
+ }
+ }
+ val mergedStyle = style.merge(
+ TextStyle(
+ color = textColor,
+ textAlign = textAlign ?: style.textAlign,
+ textDecoration = textDecoration,
+ )
+ )
+ BasicText(
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ minLines = minLines,
+ inlineContent = inlineContent,
+ )
+}
+
+public val LocalTextStyle: ProvidableCompositionLocal =
+ compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default }
+
+@Composable
+public fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
+ val mergedStyle = LocalTextStyle.current.merge(value)
+ CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/KSTheme.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/KSTheme.kt
new file mode 100644
index 00000000..43d70242
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/KSTheme.kt
@@ -0,0 +1,47 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.DarkColorScheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.KSColorScheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LightColorScheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalContentColor
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color.LocalKSColorScheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.typography.KSTypography
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.typography.LocalKSTypography
+
+@Composable
+public fun KSTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colorScheme = if (darkTheme) {
+ DarkColorScheme
+ } else {
+ LightColorScheme
+ }
+
+ CompositionLocalProvider(
+ LocalKSColorScheme provides colorScheme,
+ LocalContentColor provides colorScheme.onBackground,
+ LocalKSTypography provides KSTypography.Default,
+ ) {
+ MaterialTheme(
+ content = content,
+ )
+ }
+}
+
+public object KSTheme {
+ public val colorScheme: KSColorScheme
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalKSColorScheme.current
+ public val typography: KSTypography
+ @Composable
+ @ReadOnlyComposable
+ get() = LocalKSTypography.current
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/color/KSColorScheme.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/color/KSColorScheme.kt
new file mode 100644
index 00000000..9772a392
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/color/KSColorScheme.kt
@@ -0,0 +1,89 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.color
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+@Immutable
+public class KSColorScheme internal constructor(
+ public val primary: Color,
+ public val onPrimary: Color,
+ public val secondary: Color,
+ public val tertiary: Color,
+ public val background: Color,
+ public val onBackground: Color,
+ public val onBackgroundVariant: Color,
+ public val container: Color,
+ public val primaryOnContainer: Color,
+ public val outline: Color,
+ public val gradient: List,
+ public val isDark: Boolean,
+)
+
+internal val LightColorScheme = KSColorScheme(
+ primary = Palette.CandyGrapeFizz50,
+ onPrimary = Palette.Lavender50,
+ secondary = Palette.PromiscuousPink50,
+ tertiary = Palette.Watermelonade,
+ background = Palette.Lavender50,
+ onBackground = Palette.MysteriousDepths10,
+ onBackgroundVariant = Palette.MysteriousDepths30,
+ container = Palette.CandyDreams20,
+ primaryOnContainer = Palette.CandyGrapeFizz50,
+ outline = Palette.CandyDreams10,
+ gradient = listOf(
+ Palette.CandyGrapeFizz50,
+ Palette.PromiscuousPink50,
+ Palette.Watermelonade,
+ ),
+ isDark = false,
+)
+
+internal val DarkColorScheme = KSColorScheme(
+ primary = Palette.CandyGrapeFizz40,
+ onPrimary = Palette.Lavender40,
+ secondary = Palette.PromiscuousPink50,
+ background = Palette.MysteriousDepths10,
+ onBackground = Palette.Lavender50,
+ onBackgroundVariant = Palette.Lavender30,
+ tertiary = Palette.Watermelonade,
+ container = Palette.MysteriousDepths20,
+ primaryOnContainer = Palette.CandyGrapeFizz60,
+ outline = Palette.MysteriousDepths30,
+ gradient = listOf(
+ Palette.CandyGrapeFizz50,
+ Palette.PromiscuousPink50,
+ Palette.Watermelonade,
+ ),
+ isDark = true,
+)
+
+private object Palette {
+ val CandyGrapeFizz60 = Color(0xFF_8968FF)
+ val CandyGrapeFizz50 = Color(0xFF_7F52FF)
+ val CandyGrapeFizz40 = Color(0xFF_6E46DE)
+
+ val PromiscuousPink50 = Color(0xFF_C711E1)
+
+ val Watermelonade = Color(0xFF_E44855)
+
+ val Lavender50 = Color(0xFF_E7DBFF)
+ val Lavender40 = Color(0xFF_DBD0F2)
+ val Lavender30 = Color(0xFF_C4BAD9)
+
+ val CandyDreams20 = Color(0xFF_F1C2F9)
+ val CandyDreams10 = Color(0xFF_EDB3F7)
+
+ val MysteriousDepths30 = Color(0xFF_2C2F4E)
+ val MysteriousDepths20 = Color(0xFF_18193A)
+ val MysteriousDepths10 = Color(0xFF_070426)
+}
+
+internal val LocalKSColorScheme = staticCompositionLocalOf {
+ error("No KSColorScheme provided")
+}
+
+public val LocalContentColor: ProvidableCompositionLocal =
+ compositionLocalOf { error("No ContentColor provided") }
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkAdd.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkAdd.kt
new file mode 100644
index 00000000..37110650
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkAdd.kt
@@ -0,0 +1,51 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon
+
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+/**
+ * Copied from `androidx.compose.material.icons.outlined.BookmarkAdd`.
+ */
+public val KSIcons.BookmarkAdd: ImageVector
+ get() {
+ if (_bookmarkAdd != null) {
+ return _bookmarkAdd!!
+ }
+ _bookmarkAdd = materialIcon(name = "Outlined.BookmarkAdd") {
+ materialPath {
+ moveTo(17.0f, 11.0f)
+ verticalLineToRelative(6.97f)
+ lineToRelative(-5.0f, -2.14f)
+ lineToRelative(-5.0f, 2.14f)
+ verticalLineTo(5.0f)
+ horizontalLineToRelative(6.0f)
+ verticalLineTo(3.0f)
+ horizontalLineTo(7.0f)
+ curveTo(5.9f, 3.0f, 5.0f, 3.9f, 5.0f, 5.0f)
+ verticalLineToRelative(16.0f)
+ lineToRelative(7.0f, -3.0f)
+ lineToRelative(7.0f, 3.0f)
+ verticalLineTo(11.0f)
+ horizontalLineTo(17.0f)
+ close()
+ moveTo(21.0f, 7.0f)
+ horizontalLineToRelative(-2.0f)
+ verticalLineToRelative(2.0f)
+ horizontalLineToRelative(-2.0f)
+ verticalLineTo(7.0f)
+ horizontalLineToRelative(-2.0f)
+ verticalLineTo(5.0f)
+ horizontalLineToRelative(2.0f)
+ verticalLineTo(3.0f)
+ horizontalLineToRelative(2.0f)
+ verticalLineToRelative(2.0f)
+ horizontalLineToRelative(2.0f)
+ verticalLineTo(7.0f)
+ close()
+ }
+ }
+ return _bookmarkAdd!!
+ }
+
+private var _bookmarkAdd: ImageVector? = null
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkFill.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkFill.kt
new file mode 100644
index 00000000..a9f5c3ec
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/BookmarkFill.kt
@@ -0,0 +1,31 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon
+
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+/**
+ * Copied from `androidx.compose.material.icons.rounded.Bookmark`.
+ */
+public val KSIcons.BookmarkFill: ImageVector
+ get() {
+ if (_bookmark != null) {
+ return _bookmark!!
+ }
+ _bookmark = materialIcon(name = "Rounded.Bookmark") {
+ materialPath {
+ moveTo(17.0f, 3.0f)
+ horizontalLineTo(7.0f)
+ curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
+ verticalLineToRelative(16.0f)
+ lineToRelative(7.0f, -3.0f)
+ lineToRelative(7.0f, 3.0f)
+ verticalLineTo(5.0f)
+ curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
+ close()
+ }
+ }
+ return _bookmark!!
+ }
+
+private var _bookmark: ImageVector? = null
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/KSIcons.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/KSIcons.kt
new file mode 100644
index 00000000..e1f6cece
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/KSIcons.kt
@@ -0,0 +1,13 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.ui.graphics.vector.ImageVector
+
+public object KSIcons {
+ public val Close: ImageVector = Icons.Rounded.Close
+ public val Settings: ImageVector = Icons.Rounded.Settings
+ public val ArrowDown: ImageVector = Icons.Rounded.KeyboardArrowDown
+}
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/Sync.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/Sync.kt
new file mode 100644
index 00000000..0c1427ff
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/Sync.kt
@@ -0,0 +1,52 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon
+
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+/**
+ * Copied from `androidx.compose.material.icons.rounded.Cached`.
+ */
+public val KSIcons.Sync: ImageVector
+ get() {
+ if (_cached != null) {
+ return _cached!!
+ }
+ _cached = materialIcon(name = "Rounded.Cached") {
+ materialPath {
+ moveTo(18.65f, 8.35f)
+ lineToRelative(-2.79f, 2.79f)
+ curveToRelative(-0.32f, 0.32f, -0.1f, 0.86f, 0.35f, 0.86f)
+ horizontalLineTo(18.0f)
+ curveToRelative(0.0f, 3.31f, -2.69f, 6.0f, -6.0f, 6.0f)
+ curveToRelative(-0.79f, 0.0f, -1.56f, -0.15f, -2.25f, -0.44f)
+ curveToRelative(-0.36f, -0.15f, -0.77f, -0.04f, -1.04f, 0.23f)
+ curveToRelative(-0.51f, 0.51f, -0.33f, 1.37f, 0.34f, 1.64f)
+ curveToRelative(0.91f, 0.37f, 1.91f, 0.57f, 2.95f, 0.57f)
+ curveToRelative(4.42f, 0.0f, 8.0f, -3.58f, 8.0f, -8.0f)
+ horizontalLineToRelative(1.79f)
+ curveToRelative(0.45f, 0.0f, 0.67f, -0.54f, 0.35f, -0.85f)
+ lineToRelative(-2.79f, -2.79f)
+ curveToRelative(-0.19f, -0.2f, -0.51f, -0.2f, -0.7f, -0.01f)
+ close()
+ moveTo(6.0f, 12.0f)
+ curveToRelative(0.0f, -3.31f, 2.69f, -6.0f, 6.0f, -6.0f)
+ curveToRelative(0.79f, 0.0f, 1.56f, 0.15f, 2.25f, 0.44f)
+ curveToRelative(0.36f, 0.15f, 0.77f, 0.04f, 1.04f, -0.23f)
+ curveToRelative(0.51f, -0.51f, 0.33f, -1.37f, -0.34f, -1.64f)
+ curveTo(14.04f, 4.2f, 13.04f, 4.0f, 12.0f, 4.0f)
+ curveToRelative(-4.42f, 0.0f, -8.0f, 3.58f, -8.0f, 8.0f)
+ horizontalLineTo(2.21f)
+ curveToRelative(-0.45f, 0.0f, -0.67f, 0.54f, -0.35f, 0.85f)
+ lineToRelative(2.79f, 2.79f)
+ curveToRelative(0.2f, 0.2f, 0.51f, 0.2f, 0.71f, 0.0f)
+ lineToRelative(2.79f, -2.79f)
+ curveToRelative(0.31f, -0.31f, 0.09f, -0.85f, -0.36f, -0.85f)
+ horizontalLineTo(6.0f)
+ close()
+ }
+ }
+ return _cached!!
+ }
+
+private var _cached: ImageVector? = null
diff --git a/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/typography/KSTypography.kt b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/typography/KSTypography.kt
new file mode 100644
index 00000000..48d7c355
--- /dev/null
+++ b/android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/typography/KSTypography.kt
@@ -0,0 +1,169 @@
+package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.typography
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import io.github.reactivecircus.kstreamlined.android.designsystem.R
+
+@Immutable
+public class KSTypography internal constructor(
+ public val headlineLarge: TextStyle,
+ public val headlineMedium: TextStyle,
+ public val headlineSmall: TextStyle,
+ public val titleLarge: TextStyle,
+ public val titleMedium: TextStyle,
+ public val titleSmall: TextStyle,
+ public val bodyLarge: TextStyle,
+ public val bodyMedium: TextStyle,
+ public val bodySmall: TextStyle,
+ public val labelLarge: TextStyle,
+ public val labelMedium: TextStyle,
+ public val labelSmall: TextStyle,
+) {
+ @Suppress("CyclomaticComplexMethod")
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is KSTypography) return false
+
+ if (headlineLarge != other.headlineLarge) return false
+ if (headlineMedium != other.headlineMedium) return false
+ if (headlineSmall != other.headlineSmall) return false
+ if (titleLarge != other.titleLarge) return false
+ if (titleMedium != other.titleMedium) return false
+ if (titleSmall != other.titleSmall) return false
+ if (bodyLarge != other.bodyLarge) return false
+ if (bodyMedium != other.bodyMedium) return false
+ if (bodySmall != other.bodySmall) return false
+ if (labelLarge != other.labelLarge) return false
+ if (labelMedium != other.labelMedium) return false
+ if (labelSmall != other.labelSmall) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = headlineLarge.hashCode()
+ result = 31 * result + headlineMedium.hashCode()
+ result = 31 * result + headlineSmall.hashCode()
+ result = 31 * result + titleLarge.hashCode()
+ result = 31 * result + titleMedium.hashCode()
+ result = 31 * result + titleSmall.hashCode()
+ result = 31 * result + bodyLarge.hashCode()
+ result = 31 * result + bodyMedium.hashCode()
+ result = 31 * result + bodySmall.hashCode()
+ result = 31 * result + labelLarge.hashCode()
+ result = 31 * result + labelMedium.hashCode()
+ result = 31 * result + labelSmall.hashCode()
+ return result
+ }
+
+ @Suppress("MaxLineLength")
+ override fun toString(): String {
+ return "KSTypography(headlineLarge=$headlineLarge, headlineMedium=$headlineMedium, headlineSmall=$headlineSmall, " +
+ "titleLarge=$titleLarge, titleMedium=$titleMedium, titleSmall=$titleSmall, " +
+ "bodyLarge=$bodyLarge, bodyMedium=$bodyMedium, bodySmall=$bodySmall, " +
+ "labelLarge=$labelLarge, labelMedium=$labelMedium, labelSmall=$labelSmall)"
+ }
+
+ internal companion object {
+ internal val Default: KSTypography = KSTypography(
+ headlineLarge = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp,
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp,
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleLarge = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleMedium = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ titleSmall = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp,
+ ),
+ bodySmall = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp,
+ ),
+ labelLarge = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp,
+ ),
+ labelMedium = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ labelSmall = TextStyle(
+ fontFamily = JetBrainsMonoFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 10.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.sp,
+ ),
+ )
+ }
+}
+
+internal val LocalKSTypography = staticCompositionLocalOf { KSTypography.Default }
+
+private val JetBrainsMonoFontFamily = FontFamily(
+ Font(R.font.jetbrains_mono_regular, FontWeight.Normal),
+ Font(R.font.jetbrains_mono_medium, FontWeight.Medium),
+ Font(R.font.jetbrains_mono_bold, FontWeight.Bold),
+ Font(R.font.jetbrains_mono_extra_bold, FontWeight.ExtraBold),
+)
diff --git a/android/designsystem/src/main/res/font/jetbrains_mono_bold.ttf b/android/designsystem/src/main/res/font/jetbrains_mono_bold.ttf
new file mode 100644
index 00000000..b7484374
Binary files /dev/null and b/android/designsystem/src/main/res/font/jetbrains_mono_bold.ttf differ
diff --git a/android/designsystem/src/main/res/font/jetbrains_mono_extra_bold.ttf b/android/designsystem/src/main/res/font/jetbrains_mono_extra_bold.ttf
new file mode 100644
index 00000000..88eab2f7
Binary files /dev/null and b/android/designsystem/src/main/res/font/jetbrains_mono_extra_bold.ttf differ
diff --git a/android/designsystem/src/main/res/font/jetbrains_mono_medium.ttf b/android/designsystem/src/main/res/font/jetbrains_mono_medium.ttf
new file mode 100644
index 00000000..ad31fbd7
Binary files /dev/null and b/android/designsystem/src/main/res/font/jetbrains_mono_medium.ttf differ
diff --git a/android/designsystem/src/main/res/font/jetbrains_mono_regular.ttf b/android/designsystem/src/main/res/font/jetbrains_mono_regular.ttf
new file mode 100644
index 00000000..02bc07ea
Binary files /dev/null and b/android/designsystem/src/main/res/font/jetbrains_mono_regular.ttf differ
diff --git a/android/feature/common/build.gradle.kts b/android/feature/common/build.gradle.kts
index 831b539b..23050ab3 100644
--- a/android/feature/common/build.gradle.kts
+++ b/android/feature/common/build.gradle.kts
@@ -27,10 +27,8 @@ dependencies {
api(libs.androidx.core)
// Compose
- api(libs.androidx.compose.material3)
api(libs.androidx.compose.foundation)
- debugApi(libs.androidx.compose.ui.tooling)
- api(libs.androidx.compose.ui.toolingPreview)
+ api(libs.androidx.compose.ui.tooling)
api(libs.androidx.lifecycle.runtime)
api(libs.androidx.lifecycle.runtimeCompose)
api(libs.androidx.navigation.compose)
diff --git a/android/feature/home/build.gradle.kts b/android/feature/home/build.gradle.kts
index 3ab60a64..8a1d172f 100644
--- a/android/feature/home/build.gradle.kts
+++ b/android/feature/home/build.gradle.kts
@@ -9,4 +9,6 @@ android {
dependencies {
implementation(project(":feature:common"))
+ implementation(project(":common-ui:feed"))
+ implementation(project(":kmp:data"))
}
diff --git a/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/HomeScreen.kt b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/HomeScreen.kt
new file mode 100644
index 00000000..2fe920ca
--- /dev/null
+++ b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/HomeScreen.kt
@@ -0,0 +1,249 @@
+package io.github.reactivecircus.kstreamlined.android.feature.home
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.LinearGradientShader
+import androidx.compose.ui.graphics.Shader
+import androidx.compose.ui.graphics.ShaderBrush
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import io.github.reactivecircus.kstreamlined.android.common.ui.feed.KotlinWeeklyCard
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.FilledIconButton
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Surface
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Text
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
+import io.github.reactivecircus.kstreamlined.android.feature.home.component.FeedFilterChip
+import io.github.reactivecircus.kstreamlined.android.feature.home.component.SyncButton
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
+import kotlinx.coroutines.delay
+
+@Composable
+public fun HomeScreen(
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(KSTheme.colorScheme.background),
+ ) {
+ Surface(
+ elevation = 2.dp,
+ ) {
+ // TODO move to :designsystem
+ Column(
+ modifier = Modifier.padding(vertical = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .statusBarsPadding(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ GradientTitle(
+ text = "KStreamlined",
+ modifier = Modifier.padding(horizontal = 24.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ FilledIconButton(
+ KSIcons.Settings,
+ contentDescription = null,
+ onClick = {},
+ iconTint = KSTheme.colorScheme.primaryOnContainer,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(horizontal = 24.dp),
+ ) {
+ FeedFilterChip(selectedFeedCount = 4)
+
+ var syncing by remember { mutableStateOf(false) }
+
+ SyncButton(
+ onClick = { syncing = true },
+ syncing = syncing,
+ )
+
+ LaunchedEffect(syncing) {
+ if (syncing) {
+ @Suppress("MagicNumber")
+ (delay(500))
+ syncing = false
+ }
+ }
+ }
+ }
+ }
+
+ LazyColumn(
+ contentPadding = PaddingValues(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ item {
+ Text(
+ text = "Today",
+ style = KSTheme.typography.titleMedium,
+ color = KSTheme.colorScheme.onBackgroundVariant,
+ )
+ }
+ item {
+ var item by remember {
+ mutableStateOf(
+ FeedItem.KotlinWeekly(
+ id = "1",
+ title = "Kotlin Weekly #381",
+ publishTime = "Moments ago",
+ contentUrl = "contentUrl",
+ savedForLater = false,
+ )
+ )
+ }
+ KotlinWeeklyCard(
+ item = item,
+ onItemClick = {},
+ onSaveButtonClick = {
+ item = item.copy(savedForLater = !item.savedForLater)
+ },
+ )
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.container,
+ ) {}
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.container,
+ ) {}
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.container,
+ ) {}
+ }
+
+ item {
+ Text(
+ text = "This week",
+ style = KSTheme.typography.titleMedium,
+ color = KSTheme.colorScheme.onBackgroundVariant,
+ )
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.onBackgroundVariant,
+ ) {}
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.primary,
+ ) {}
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.secondary,
+ ) {}
+ }
+
+ item {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ shape = RoundedCornerShape(16.dp),
+ color = KSTheme.colorScheme.tertiary,
+ ) {}
+ }
+ }
+ }
+}
+
+@Composable
+private fun GradientTitle(
+ text: String,
+ modifier: Modifier = Modifier,
+) {
+ val gradient = KSTheme.colorScheme.gradient
+ val brush = remember {
+ object : ShaderBrush() {
+ override fun createShader(size: Size): Shader {
+ return LinearGradientShader(
+ colors = gradient,
+ from = Offset(0f, size.height),
+ to = Offset(size.width * GradientHorizontalScale, 0f),
+ )
+ }
+ }
+ }
+ Text(
+ text = text,
+ style = KSTheme.typography.headlineMedium.copy(
+ fontWeight = FontWeight.ExtraBold,
+ brush = brush,
+ ),
+ modifier = modifier,
+ maxLines = 1,
+ )
+}
+
+private const val GradientHorizontalScale = 1.3f
diff --git a/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/FeedFilterChip.kt b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/FeedFilterChip.kt
new file mode 100644
index 00000000..aa5e16e0
--- /dev/null
+++ b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/FeedFilterChip.kt
@@ -0,0 +1,49 @@
+package io.github.reactivecircus.kstreamlined.android.feature.home.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Icon
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Surface
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Text
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
+
+@Composable
+internal fun FeedFilterChip(
+ selectedFeedCount: Int,
+ modifier: Modifier = Modifier,
+) {
+ Surface(
+ onClick = {},
+ modifier = modifier,
+ shape = CircleShape,
+ color = KSTheme.colorScheme.container,
+ contentColor = KSTheme.colorScheme.primaryOnContainer,
+ ) {
+ Row(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 12.dp,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = "$selectedFeedCount Feeds selected".uppercase(), // TODO use string resource
+ style = KSTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.ExtraBold,
+ letterSpacing = 0.sp,
+ ),
+ )
+ Icon(KSIcons.ArrowDown, contentDescription = null)
+ }
+ }
+}
diff --git a/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/SyncButton.kt b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/SyncButton.kt
new file mode 100644
index 00000000..23941d65
--- /dev/null
+++ b/android/feature/home/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/home/component/SyncButton.kt
@@ -0,0 +1,95 @@
+package io.github.reactivecircus.kstreamlined.android.feature.home.component
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Icon
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Surface
+import io.github.reactivecircus.kstreamlined.android.designsystem.component.Text
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
+import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Sync
+
+@Composable
+internal fun SyncButton(
+ onClick: () -> Unit,
+ syncing: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ Surface(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = !syncing,
+ shape = CircleShape,
+ color = KSTheme.colorScheme.container,
+ contentColor = KSTheme.colorScheme.primaryOnContainer,
+ ) {
+ Row(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 12.dp,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ var currentRotation by remember { mutableStateOf(0f) }
+ val rotation = remember(syncing) { Animatable(currentRotation) }
+ LaunchedEffect(syncing) {
+ if (syncing) {
+ rotation.animateTo(
+ targetValue = 360f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(AnimationDurationMillis, easing = LinearEasing),
+ ),
+ ) {
+ currentRotation = value
+ }
+ } else {
+ if (currentRotation > 0f) {
+ rotation.animateTo(
+ targetValue = 360f,
+ animationSpec = tween(
+ durationMillis = AnimationDurationMillis,
+ easing = LinearOutSlowInEasing,
+ ),
+ ) {
+ currentRotation = 0f
+ }
+ }
+ }
+ }
+ Icon(
+ KSIcons.Sync,
+ contentDescription = null,
+ modifier = Modifier.rotate(rotation.value),
+ )
+ Text(
+ text = "Sync".uppercase(),
+ style = KSTheme.typography.labelMedium.copy(
+ fontWeight = FontWeight.ExtraBold,
+ letterSpacing = 0.sp,
+ ),
+ )
+ }
+ }
+}
+
+private const val AnimationDurationMillis = 1000
diff --git a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/KMPBuildLogic.kt b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/KMPBuildLogic.kt
index f146b597..6ef46beb 100644
--- a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/KMPBuildLogic.kt
+++ b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/KMPBuildLogic.kt
@@ -4,7 +4,6 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
* Apply common configs to KMP project.
diff --git a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/SlimTests.kt b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/SlimTests.kt
index 031b474f..9c7251c6 100644
--- a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/SlimTests.kt
+++ b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/SlimTests.kt
@@ -18,7 +18,7 @@ import org.gradle.kotlin.dsl.findByType
* in Android App and Library projects, and all tests in JVM projects and Kotlin Multiplatform projects.
*/
internal fun Project.configureSlimTests() {
- if (providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
+ if (providers.gradleProperty(SlimTestsProperty).isPresent) {
// disable unit test tasks on the release, benchmark build types for Android Library projects
extensions.findByType()?.run {
beforeVariants(selector().withBuildType("release")) {
@@ -50,4 +50,4 @@ internal fun Project.configureSlimTests() {
}
}
-private const val SLIM_TESTS_PROPERTY = "slimTests"
+private const val SlimTestsProperty = "slimTests"
diff --git a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/convention/KMPCommonConventionPlugin.kt b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/convention/KMPCommonConventionPlugin.kt
index 4f0497a5..c6234b58 100644
--- a/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/convention/KMPCommonConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/io/github/reactivecircus/kstreamlined/buildlogic/convention/KMPCommonConventionPlugin.kt
@@ -1,10 +1,10 @@
package io.github.reactivecircus.kstreamlined.buildlogic.convention
import io.github.reactivecircus.kstreamlined.buildlogic.configureDetekt
-import io.github.reactivecircus.kstreamlined.buildlogic.enableExplicitApi
import io.github.reactivecircus.kstreamlined.buildlogic.configureKMPCommon
import io.github.reactivecircus.kstreamlined.buildlogic.configureKotlinCommonCompileOptions
import io.github.reactivecircus.kstreamlined.buildlogic.configureTest
+import io.github.reactivecircus.kstreamlined.buildlogic.enableExplicitApi
import io.github.reactivecircus.kstreamlined.buildlogic.markNonCompatibleConfigurationCacheTasks
import org.gradle.api.Plugin
import org.gradle.api.Project
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 565b4b10..33338da6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -84,7 +84,6 @@ firebase-remoteConfig = { module = "com.google.firebase:firebase-config-ktx", ve
firebase-perf = { module = "com.google.firebase:firebase-perf-ktx", version.ref = "firebase-perf" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "firebase-crashlytics" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose-ui" }
-androidx-compose-ui-toolingPreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose-ui" }
androidx-compose-ui-testJunit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose-ui" }
androidx-compose-ui-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose-ui" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-foundation" }
diff --git a/kmp/data/build.gradle.kts b/kmp/data/build.gradle.kts
index d6dc2664..46ddec1d 100644
--- a/kmp/data/build.gradle.kts
+++ b/kmp/data/build.gradle.kts
@@ -9,6 +9,7 @@ kotlin {
dependencies {
implementation(project(":kmp:feed-datasource:common"))
implementation(project(":kmp:persistence"))
+ api(project(":kmp:model"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kermit)
}
diff --git a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt
index 560e14ee..6f2eb28d 100644
--- a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt
+++ b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt
@@ -1,9 +1,9 @@
package io.github.reactivecircus.kstreamlined.kmp.data.feed
import co.touchlab.kermit.Logger
-import io.github.reactivecircus.kstreamlined.kmp.data.feed.model.FeedItem
-import io.github.reactivecircus.kstreamlined.kmp.data.feed.model.FeedOrigin
import io.github.reactivecircus.kstreamlined.kmp.feed.datasource.FeedDataSource
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedOrigin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
diff --git a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedSyncState.kt b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedSyncState.kt
index a82a868f..9f69d7fe 100644
--- a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedSyncState.kt
+++ b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedSyncState.kt
@@ -1,7 +1,7 @@
package io.github.reactivecircus.kstreamlined.kmp.data.feed
-import io.github.reactivecircus.kstreamlined.kmp.data.feed.model.FeedItem
-import io.github.reactivecircus.kstreamlined.kmp.data.feed.model.FeedOrigin
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
+import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedOrigin
public data class FeedSyncState(
val feedOrigins: List,
diff --git a/kmp/model/build.gradle.kts b/kmp/model/build.gradle.kts
new file mode 100644
index 00000000..693f10c3
--- /dev/null
+++ b/kmp/model/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("kstreamlined.kmp.common")
+}
diff --git a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedItem.kt b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt
similarity index 95%
rename from kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedItem.kt
rename to kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt
index e4fd9042..80304bcc 100644
--- a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedItem.kt
+++ b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt
@@ -1,4 +1,4 @@
-package io.github.reactivecircus.kstreamlined.kmp.data.feed.model
+package io.github.reactivecircus.kstreamlined.kmp.model.feed
public sealed interface FeedItem {
public val id: String
diff --git a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedOrigin.kt b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedOrigin.kt
similarity index 80%
rename from kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedOrigin.kt
rename to kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedOrigin.kt
index 5b0d1202..1daab877 100644
--- a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/model/FeedOrigin.kt
+++ b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedOrigin.kt
@@ -1,4 +1,4 @@
-package io.github.reactivecircus.kstreamlined.kmp.data.feed.model
+package io.github.reactivecircus.kstreamlined.kmp.model.feed
public data class FeedOrigin(
val key: Key,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a8f3a9a4..c5bf6a4f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -65,6 +65,7 @@ include(":kmp:feed-datasource:cloud")
include(":kmp:feed-datasource:edge")
include(":kmp:feed-datasource:testing")
include(":kmp:persistence")
+include(":kmp:model")
include(":kmp:core-utils")
include(":kmp:test-utils")
@@ -74,6 +75,7 @@ if (!isXCFrameworkBuild) {
includeProject(":app", "android/app")
includeProject(":feature:common", "android/feature/common")
includeProject(":feature:home", "android/feature/home")
+ includeProject(":common-ui:feed", "android/common-ui/feed")
includeProject(":designsystem", "android/designsystem")
}