diff --git a/README.md b/README.md index da934be..0d14f2b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ ## Getting Started After using this repo as a template you have to do the following changes so you can start working on your project - -### [settings.gradle.kts](settings.gradle.kts) -- Change `rootProject.name` to your projects name - - -## Android +---- +## Compose ### [Build.gradle.kts](app/composeApp/build.gradle.kts)] - Change `namespace` and `applicationId` to your projects name +### [settings.gradle.kts](settings.gradle.kts) +- Change `rootProject.name` to your projects name ## iOS ### [Config.xcconfig](app/iosApp/Configuration/Config.xcconfig) diff --git a/buildlogic/convention/src/main/kotlin/ComposeMultiplatformConventionPlugin.kt b/buildlogic/convention/src/main/kotlin/ComposeMultiplatformConventionPlugin.kt index db357d2..397eeec 100644 --- a/buildlogic/convention/src/main/kotlin/ComposeMultiplatformConventionPlugin.kt +++ b/buildlogic/convention/src/main/kotlin/ComposeMultiplatformConventionPlugin.kt @@ -20,7 +20,7 @@ class ComposeMultiplatformConventionPlugin : Plugin { androidMain { dependencies { implementation(composeDeps.preview) - implementation(libs.findLibrary("libs.androidx.activity.compose").get()) + implementation(libs.findLibrary("androidx.activity.compose").get()) } } commonMain { diff --git a/buildlogic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt b/buildlogic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt index 3ccbda4..5bb5a43 100644 --- a/buildlogic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt +++ b/buildlogic/convention/src/main/kotlin/KotlinMultiplatformConventionPlugin.kt @@ -12,7 +12,6 @@ class KotlinMultiplatformConventionPlugin : Plugin { with(target) { with(pluginManager) { apply(libs.findPlugin("androidLibrary").get().get().pluginId) - apply(libs.findPlugin("androidMultiplatform").get().get().pluginId) apply(libs.findPlugin("kotlinMultiplatform").get().get().pluginId) apply(libs.findPlugin("kotlinSerialization").get().get().pluginId) } diff --git a/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinAndroid.kt b/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinAndroid.kt index e839f63..8ad43c6 100644 --- a/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinAndroid.kt +++ b/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinAndroid.kt @@ -26,8 +26,8 @@ internal fun Project.configureKotlinAndroid( sourceSets["main"].resources.srcDirs("src/commonMain/composeResources") compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } packaging { resources { diff --git a/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinMultiplatform.kt b/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinMultiplatform.kt index ba71da1..7ff54e2 100644 --- a/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinMultiplatform.kt +++ b/buildlogic/convention/src/main/kotlin/dev/carlosmartino/plugins/KotlinMultiplatform.kt @@ -11,26 +11,36 @@ internal fun Project.configureKotlinMultiplatform( ) = extension.apply { applyDefaultHierarchyTemplate() - jvmToolchain(17) + jvmToolchain(21) androidTarget { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_21) } } - listOf(iosArm64(), iosSimulatorArm64()) + listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "shared" + isStatic = true + } + } + + sourceSets.all { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3Api") + optIn("kotlin.time.ExperimentalTime") + } + } sourceSets.apply { commonMain { dependencies { implementation(libs.findLibrary("kotlinx.coroutines.core").get()) - api(libs.findLibrary("koin.core").get()) } androidMain { dependencies { - implementation(libs.findLibrary("koin.android").get()) implementation(libs.findLibrary("kotlinx.coroutines.android").get()) } } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 0000000..87c84ba --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,46 @@ +group = "dev.carlosmartino.core" + +plugins { + alias(libs.plugins.kotlinDevKit) + alias(libs.plugins.composeDevKit) +} + +kotlin { + sourceSets { + commonMain.dependencies { + api(libs.atomicfu) + api(libs.kotlinx.datetime) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.json) + api(libs.koin.core) + api(libs.koin.compose) + api(libs.koin.compose.viewmodel) + api(libs.koin.compose.viewmodel.navigation) + + implementation(libs.androidx.security.crypto) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.datastore.preferences.core) + implementation(libs.multiplatform.settings.core) + implementation(libs.multiplatform.settings.noarg) + implementation(libs.multiplatform.settings.coroutines) + implementation(libs.multiplatform.settings.serialization) + implementation(libs.moko.permissions) + implementation(libs.moko.permissions.compose) + implementation(libs.moko.permissions.bluetooth) + implementation(libs.moko.permissions.camera) + implementation(libs.moko.permissions.contacts) + implementation(libs.moko.permissions.gallery) + implementation(libs.moko.permissions.location) + implementation(libs.moko.permissions.microphone) + implementation(libs.moko.permissions.motion) + implementation(libs.moko.permissions.notifications) + implementation(libs.moko.permissions.storage) + } + + androidMain.dependencies { + api(libs.kotlinx.coroutines.android) + api(libs.koin.android) + api(libs.multiplatform.settings.core) + } + } +} diff --git a/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/Platform.android.kt b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/Platform.android.kt new file mode 100644 index 0000000..bf87ba2 --- /dev/null +++ b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/Platform.android.kt @@ -0,0 +1,6 @@ +package cl.homelogic.platform.common + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual object Platform { + actual fun getType() = PlatformTypes.Android +} diff --git a/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/logging/Trace.android.kt b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/logging/Trace.android.kt new file mode 100644 index 0000000..5358804 --- /dev/null +++ b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/logging/Trace.android.kt @@ -0,0 +1,18 @@ +package cl.homelogic.platform.common.logging + +import android.util.Log + +internal actual fun logPlatform( + severity: Severity, + tag: String, + message: String, +) { + when (severity) { + Severity.Debug -> Log.d(tag, message) + Severity.Verbose -> Log.v(tag, message) + Severity.Info -> Log.i(tag, message) + Severity.Warn -> Log.w(tag, message) + Severity.Error -> Log.e(tag, message) + else -> {} + } +} diff --git a/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.android.kt b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.android.kt new file mode 100644 index 0000000..e08242f --- /dev/null +++ b/core/common/src/androidMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.android.kt @@ -0,0 +1,30 @@ +package cl.homelogic.platform.common.settings + +import android.content.Context +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.russhwolf.settings.Settings +import com.russhwolf.settings.SharedPreferencesSettings + +class AndroidSettingsFactory( + private val context: Context, +) : ISettingsFactory { + override fun createSettings(name: String?): Settings { + val prefName = name ?: "secure_settings" + + val masterKey = MasterKey + .Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + val sharedPreferences = EncryptedSharedPreferences.create( + context, + prefName, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + return SharedPreferencesSettings(sharedPreferences) + } +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/Platform.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/Platform.kt new file mode 100644 index 0000000..dcd50ec --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/Platform.kt @@ -0,0 +1,11 @@ +package cl.homelogic.platform.common + +enum class PlatformTypes { + IOS, + Android, +} + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +expect object Platform { + fun getType(): PlatformTypes +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/Trace.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/Trace.kt new file mode 100644 index 0000000..476b096 --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/Trace.kt @@ -0,0 +1,230 @@ +package cl.homelogic.platform.common.logging + +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +enum class Severity { + Debug, + Verbose, + Info, + Warn, + Error, + None, +} + +enum class TreeRoots { + Navigation, + Koin, + AssetsParser, + DesignSystem, + Settings, +} + +private data class LogEntry( + val timestamp: Instant, + val severity: Severity, + val tag: String, + val message: String, +) + +private interface LogTree { + val maxEntries: Int + val logs: ArrayDeque + + fun log( + severity: Severity, + tag: String, + message: String, + ) + + fun getFormattedLogs(): String +} + +private class MemoryLogTree( + override val maxEntries: Int, +) : LogTree { + override val logs = ArrayDeque(maxEntries) + private val logsLock = SynchronizedObject() + + override fun log( + severity: Severity, + tag: String, + message: String, + ) { + synchronized(logsLock) { + if (logs.size >= maxEntries) { + logs.removeFirst() + } + logs.addLast(LogEntry(Clock.System.now(), severity, tag, message)) + } + } + + override fun getFormattedLogs(): String = + buildString { + synchronized(logsLock) { + logs.forEach { entry -> + appendLine( + "${entry.timestamp.toLocalDateTime(TimeZone.currentSystemDefault())} " + + "${entry.severity.name.padEnd(7)} " + + "${entry.tag}: ${entry.message}", + ) + } + } + } +} + +internal expect fun logPlatform( + severity: Severity, + tag: String, + message: String, +) + +class Trace { + companion object { + private const val DEFAULT_LOG_MESSAGE = "The message content was empty" + + private var _isDebug: Boolean = false + private var _minSeverity: Severity = Severity.None + private var _treeSize: Int = 20 + private val _trees = HashMap() + + fun initialize(isDebug: Boolean) { + _isDebug = isDebug + _minSeverity = if (_isDebug) Severity.Verbose else Severity.None + plantTree(TreeRoots.Navigation) + plantTree(TreeRoots.Koin) + plantTree(TreeRoots.AssetsParser) + plantTree(TreeRoots.DesignSystem) + plantTree(TreeRoots.Settings) + } + + fun setLoggingSeverity(severity: Severity) { + _minSeverity = severity + } + + fun setTreeSizes(size: Int) { + _treeSize = size + } + + private fun plantTree(name: TreeRoots) { + _trees[name.name] = MemoryLogTree(_treeSize) + } + + fun plantTree(name: String) { + _trees[name] = MemoryLogTree(_treeSize) + } + + fun getTreeContent(name: String): String = _trees[name]?.getFormattedLogs() ?: "Tree '$name' not found" + + private fun log( + severity: Severity, + tag: String, + message: String, + ) { + if (severity == Severity.None || severity.ordinal < _minSeverity.ordinal) return + logPlatform(severity, "Trace - $tag", message) + _trees.values.forEach { tree -> + tree.log(severity, tag, message) + } + } + + fun v( + tag: TreeRoots, + message: String, + ) { + v(tag.name, message) + } + + fun d( + tag: TreeRoots, + message: String, + ) { + d(tag.name, message) + } + + fun i( + tag: TreeRoots, + message: String, + ) { + i(tag.name, message) + } + + fun w( + tag: TreeRoots, + message: String, + ) { + w(tag.name, message) + } + + fun e( + tag: TreeRoots, + message: String, + ) { + e(tag.name, message) + } + + fun v( + tag: String, + message: String?, + ) { + log(Severity.Verbose, tag, message ?: DEFAULT_LOG_MESSAGE) + } + + fun d( + tag: String, + message: String?, + ) { + log(Severity.Debug, tag, message ?: DEFAULT_LOG_MESSAGE) + } + + fun i( + tag: String, + message: String?, + ) { + log(Severity.Info, tag, message ?: DEFAULT_LOG_MESSAGE) + } + + fun w( + tag: String, + message: String?, + ) { + log(Severity.Warn, tag, message ?: DEFAULT_LOG_MESSAGE) + } + + fun e( + tag: String, + message: String?, + ) { + log(Severity.Error, tag, message ?: DEFAULT_LOG_MESSAGE) + } + + fun e( + tag: String, + message: String, + throwable: Throwable, + ) { + log( + Severity.Error, + tag, + (message + "\n" + throwable.message + "\n" + throwable.stackTraceToString()), + ) + } + + fun e( + tag: String, + message: String, + exception: Exception, + ) { + log( + Severity.Error, + tag, + (message + "\n" + exception.message + "\n" + exception.stackTraceToString()), + ) + } + } +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/TraceKoinLogger.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/TraceKoinLogger.kt new file mode 100644 index 0000000..caa35b1 --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/logging/TraceKoinLogger.kt @@ -0,0 +1,24 @@ +package cl.homelogic.platform.common.logging + +import org.koin.core.logger.Level +import org.koin.core.logger.Logger +import org.koin.core.logger.MESSAGE + +class TraceKoinLogger : Logger() { + override fun display( + level: Level, + msg: MESSAGE, + ) { + when (level) { + Level.DEBUG -> Trace.d(DEFAULT_TAG, msg) + Level.INFO -> Trace.i(DEFAULT_TAG, msg) + Level.WARNING -> Trace.w(DEFAULT_TAG, msg) + Level.ERROR -> Trace.e(DEFAULT_TAG, msg) + Level.NONE -> {} + } + } + + companion object { + const val DEFAULT_TAG = "Koin" + } +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/PermissionManager.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/PermissionManager.kt new file mode 100644 index 0000000..90c1f5a --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/PermissionManager.kt @@ -0,0 +1,77 @@ +package cl.homelogic.platform.common.permissions + +import cl.homelogic.platform.common.permissions.PermissionStatus.Denied +import cl.homelogic.platform.common.permissions.PermissionStatus.Granted +import cl.homelogic.platform.common.permissions.PermissionStatus.NotGranted +import dev.icerock.moko.permissions.Permission +import dev.icerock.moko.permissions.PermissionState +import dev.icerock.moko.permissions.bluetooth.BluetoothAdvertisePermission +import dev.icerock.moko.permissions.bluetooth.BluetoothConnectPermission +import dev.icerock.moko.permissions.bluetooth.BluetoothLEPermission +import dev.icerock.moko.permissions.bluetooth.BluetoothScanPermission +import dev.icerock.moko.permissions.camera.CameraPermission +import dev.icerock.moko.permissions.contacts.ContactPermission +import dev.icerock.moko.permissions.gallery.GalleryPermission +import dev.icerock.moko.permissions.location.BackgroundLocationPermission +import dev.icerock.moko.permissions.location.CoarseLocationPermission +import dev.icerock.moko.permissions.location.LocationPermission +import dev.icerock.moko.permissions.microphone.RecordAudioPermission +import dev.icerock.moko.permissions.motion.MotionPermission +import dev.icerock.moko.permissions.notifications.RemoteNotificationPermission +import dev.icerock.moko.permissions.storage.StoragePermission +import dev.icerock.moko.permissions.storage.WriteStoragePermission + +enum class PermissionType { + CAMERA, + GALLERY, + STORAGE, + WRITE_STORAGE, + LOCATION, + COARSE_LOCATION, + BACKGROUND_LOCATION, + BLUETOOTH_LE, + REMOTE_NOTIFICATION, + RECORD_AUDIO, + BLUETOOTH_SCAN, + BLUETOOTH_ADVERTISE, + BLUETOOTH_CONNECT, + CONTACTS, + MOTION, + ; + + companion object { + fun PermissionType.toMokoPermission() = + when (this) { + CAMERA -> CameraPermission + GALLERY -> GalleryPermission + STORAGE -> StoragePermission + WRITE_STORAGE -> WriteStoragePermission + LOCATION -> LocationPermission + COARSE_LOCATION -> CoarseLocationPermission + BACKGROUND_LOCATION -> BackgroundLocationPermission + BLUETOOTH_LE -> BluetoothLEPermission + REMOTE_NOTIFICATION -> RemoteNotificationPermission + RECORD_AUDIO -> RecordAudioPermission + BLUETOOTH_SCAN -> BluetoothScanPermission + BLUETOOTH_ADVERTISE -> BluetoothAdvertisePermission + BLUETOOTH_CONNECT -> BluetoothConnectPermission + CONTACTS -> ContactPermission + MOTION -> MotionPermission + } + } +} + +fun PermissionState.toPermissionStatus() = + when (this) { + PermissionState.NotDetermined -> NotGranted + PermissionState.NotGranted -> NotGranted + PermissionState.Granted -> Granted + PermissionState.Denied -> Denied + PermissionState.DeniedAlways -> Denied + } + +enum class PermissionStatus { + NotGranted, + Granted, + Denied, +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/composables/PermissionSystem.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/composables/PermissionSystem.kt new file mode 100644 index 0000000..3d77d88 --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/permissions/composables/PermissionSystem.kt @@ -0,0 +1,74 @@ +package cl.homelogic.platform.common.permissions.composables + +import androidx.compose.foundation.layout.LayoutScopeMarker +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import cl.homelogic.platform.common.permissions.PermissionStatus +import cl.homelogic.platform.common.permissions.PermissionType +import cl.homelogic.platform.common.permissions.PermissionType.Companion.toMokoPermission +import cl.homelogic.platform.common.permissions.toPermissionStatus +import dev.icerock.moko.permissions.DeniedAlwaysException +import dev.icerock.moko.permissions.DeniedException +import dev.icerock.moko.permissions.PermissionsController +import dev.icerock.moko.permissions.RequestCanceledException +import dev.icerock.moko.permissions.compose.PermissionsControllerFactory +import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory + +@Composable +fun PermissionSystem( + content: @Composable PermissionsScope.() -> Unit, +) { + val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory() + val controller: PermissionsController = remember(factory) { + factory.createPermissionsController() + } + + PermissionsScopeInstance(controller).content() +} + +@LayoutScopeMarker +@Immutable +interface PermissionsScope { + suspend fun requestPermission(permission: PermissionType): PermissionStatus + + suspend fun isPermissionGranted(permission: PermissionType): Boolean + + suspend fun getPermissionState(permission: PermissionType): PermissionStatus + + fun openAppSettings() +} + +internal class PermissionsScopeInstance( + private val permissionsController: PermissionsController, +) : PermissionsScope { + override suspend fun requestPermission(permission: PermissionType): PermissionStatus { + try { + permissionsController.providePermission(permission.toMokoPermission()) + return PermissionStatus.Granted + } catch (deniedAlways: DeniedAlwaysException) { + return PermissionStatus.Denied + } catch (denied: DeniedException) { + return PermissionStatus.Denied + } catch (canceled: RequestCanceledException) { + return PermissionStatus.NotGranted + } catch (exception: Exception) { + return PermissionStatus.NotGranted + } + } + + override suspend fun isPermissionGranted(permission: PermissionType): Boolean = + permissionsController.isPermissionGranted( + permission.toMokoPermission(), + ) + + override suspend fun getPermissionState(permission: PermissionType): PermissionStatus = + permissionsController + .getPermissionState( + permission.toMokoPermission(), + ).toPermissionStatus() + + override fun openAppSettings() { + permissionsController.openAppSettings() + } +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.kt new file mode 100644 index 0000000..2387952 --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.kt @@ -0,0 +1,57 @@ +package cl.homelogic.platform.common.settings + +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots +import com.russhwolf.settings.Settings + +interface ISettingsSource { + fun get( + key: PreferenceKey, + defaultValue: T, + ): T + + fun set( + key: PreferenceKey, + value: T, + ) + + fun remove(key: PreferenceKey) + + fun clear() +} + +class SettingsSource( + private val settings: Settings, +) : ISettingsSource { + // TODO: add fuction to check if key exists + + override fun get( + key: PreferenceKey, + defaultValue: T, + ): T { + Trace.d(TreeRoots.Settings, "Retrieving setting: ${key.key}") + return key.getValue(settings, defaultValue) + } + + override fun set( + key: PreferenceKey, + value: T, + ) { + Trace.d(TreeRoots.Settings, "Storing setting: ${key.key} : $value") + key.setValue(settings, value) + } + + override fun remove(key: PreferenceKey) { + Trace.d(TreeRoots.Settings, "Removing setting: ${key.key}") + settings.remove(key.key) + } + + override fun clear() { + Trace.d(TreeRoots.Settings, "Deleting all settings") + settings.clear() + } +} + +interface ISettingsFactory { + fun createSettings(name: String? = null): Settings +} diff --git a/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsStructure.kt b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsStructure.kt new file mode 100644 index 0000000..b20b0c3 --- /dev/null +++ b/core/common/src/commonMain/kotlin/cl/homelogic/platform/common/settings/SettingsStructure.kt @@ -0,0 +1,101 @@ +package cl.homelogic.platform.common.settings + +import com.russhwolf.settings.Settings + +sealed class PreferenceKey( + val key: String, +) { + abstract fun getValue( + settings: Settings, + defaultValue: T, + ): T + + abstract fun setValue( + settings: Settings, + value: T, + ) + + class StringKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: String, + ): String = settings.getString(key, defaultValue) + + override fun setValue( + settings: Settings, + value: String, + ) = settings.putString(key, value) + } + + class IntKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: Int, + ): Int = settings.getInt(key, defaultValue) + + override fun setValue( + settings: Settings, + value: Int, + ) = settings.putInt(key, value) + } + + class LongKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: Long, + ): Long = settings.getLong(key, defaultValue) + + override fun setValue( + settings: Settings, + value: Long, + ) = settings.putLong(key, value) + } + + class FloatKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: Float, + ): Float = settings.getFloat(key, defaultValue) + + override fun setValue( + settings: Settings, + value: Float, + ) = settings.putFloat(key, value) + } + + class DoubleKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: Double, + ): Double = settings.getDouble(key, defaultValue) + + override fun setValue( + settings: Settings, + value: Double, + ) = settings.putDouble(key, value) + } + + class BooleanKey( + key: String, + ) : PreferenceKey(key) { + override fun getValue( + settings: Settings, + defaultValue: Boolean, + ): Boolean = settings.getBoolean(key, defaultValue) + + override fun setValue( + settings: Settings, + value: Boolean, + ) = settings.putBoolean(key, value) + } +} diff --git a/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/Platform.ios.kt b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/Platform.ios.kt new file mode 100644 index 0000000..db08433 --- /dev/null +++ b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/Platform.ios.kt @@ -0,0 +1,6 @@ +package cl.homelogic.platform.common + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual object Platform { + actual fun getType() = PlatformTypes.IOS +} diff --git a/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/logging/Trace.ios.kt b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/logging/Trace.ios.kt new file mode 100644 index 0000000..90dd733 --- /dev/null +++ b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/logging/Trace.ios.kt @@ -0,0 +1,12 @@ +package cl.homelogic.platform.common.logging + +import platform.Foundation.NSLog + +internal actual fun logPlatform( + severity: Severity, + tag: String, + message: String, +) { + if (severity == Severity.None) return + NSLog("Trace - $tag [${severity.name}]: $message") +} diff --git a/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.ios.kt b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.ios.kt new file mode 100644 index 0000000..d4c144e --- /dev/null +++ b/core/common/src/iosMain/kotlin/cl/homelogic/platform/common/settings/SettingsSource.ios.kt @@ -0,0 +1,16 @@ +package cl.homelogic.platform.common.settings + +import com.russhwolf.settings.NSUserDefaultsSettings +import com.russhwolf.settings.Settings +import platform.Foundation.NSUserDefaults + +class IOSSettingsFactory : ISettingsFactory { + override fun createSettings(name: String?): Settings { + val userDefaults = if (name != null) { + NSUserDefaults(suiteName = name) + } else { + NSUserDefaults.standardUserDefaults + } + return NSUserDefaultsSettings(userDefaults) + } +} diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 0000000..dccbcc3 --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,22 @@ +group = "dev.carlosmartino.designsystem" + +plugins { + alias(libs.plugins.kotlinDevKit) + alias(libs.plugins.composeDevKit) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(projects.core.common) + api(libs.insetsx) + implementation(libs.markdown) + } + } +} + +compose.resources { + publicResClass = true + packageOfResClass = "dev.carlosmartino.designsystem.resources" + generateResClass = always +} diff --git a/core/designsystem/src/androidMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.android.kt b/core/designsystem/src/androidMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.android.kt new file mode 100644 index 0000000..1f3263a --- /dev/null +++ b/core/designsystem/src/androidMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.android.kt @@ -0,0 +1,39 @@ +package cl.homelogic.platform.designsystem.themes + +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +enum class StatusBarState { + LIGHT, + DARK, +} + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class StatusBarManager : IStatusBarManager { + private val _coroutineScope = CoroutineScope(Dispatchers.Default) + + companion object { + private val _statusBarState = MutableStateFlow(StatusBarState.LIGHT) + val state: StateFlow = _statusBarState.asStateFlow() + } + + override fun lightMode() { + Trace.d(TreeRoots.DesignSystem, "Android - requesting light status bar") + _coroutineScope.launch { + _statusBarState.emit(StatusBarState.LIGHT) + } + } + + override fun darkMode() { + Trace.d(TreeRoots.DesignSystem, "Android - requesting dark status bar") + _coroutineScope.launch { + _statusBarState.emit(StatusBarState.DARK) + } + } +} diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-Bold.ttf new file mode 100644 index 0000000..98c1c3d Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraBold.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraBold.ttf new file mode 100644 index 0000000..369d719 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraLight.ttf new file mode 100644 index 0000000..8915d96 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-Light.ttf new file mode 100644 index 0000000..4942924 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-Medium.ttf new file mode 100644 index 0000000..5eda9ec Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-Regular.ttf new file mode 100644 index 0000000..1a07233 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/Manrope-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/Manrope-SemiBold.ttf new file mode 100644 index 0000000..b6e9c20 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/Manrope-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Bold.ttf new file mode 100644 index 0000000..9147fd3 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-ExtraLight.ttf new file mode 100644 index 0000000..7b3160e Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Light.ttf new file mode 100644 index 0000000..9fbc5e8 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Medium.ttf new file mode 100644 index 0000000..49bb0f6 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Regular.ttf new file mode 100644 index 0000000..93da980 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-SemiBold.ttf new file mode 100644 index 0000000..f817212 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Thin.ttf new file mode 100644 index 0000000..a8d0472 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Bold.ttf new file mode 100644 index 0000000..8dbdcdd Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-ExtraLight.ttf new file mode 100644 index 0000000..14a25e7 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Light.ttf new file mode 100644 index 0000000..36e2303 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Medium.ttf new file mode 100644 index 0000000..ff9985c Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Regular.ttf new file mode 100644 index 0000000..3a35182 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-SemiBold.ttf new file mode 100644 index 0000000..0a9cfda Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Thin.ttf new file mode 100644 index 0000000..dd101e6 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsOutlined_Filled-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Bold.ttf new file mode 100644 index 0000000..c557821 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-ExtraLight.ttf new file mode 100644 index 0000000..8f16aad Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Light.ttf new file mode 100644 index 0000000..616cc6d Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Medium.ttf new file mode 100644 index 0000000..3131034 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Regular.ttf new file mode 100644 index 0000000..10aa9e5 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-SemiBold.ttf new file mode 100644 index 0000000..3a0ebae Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Thin.ttf new file mode 100644 index 0000000..6d0d193 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Bold.ttf new file mode 100644 index 0000000..52756dd Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-ExtraLight.ttf new file mode 100644 index 0000000..602143f Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Light.ttf new file mode 100644 index 0000000..f2b9493 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Medium.ttf new file mode 100644 index 0000000..ce89979 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Regular.ttf new file mode 100644 index 0000000..ba348e7 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-SemiBold.ttf new file mode 100644 index 0000000..e74f221 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Thin.ttf new file mode 100644 index 0000000..534e110 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsRounded_Filled-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Bold.ttf new file mode 100644 index 0000000..b896e67 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-ExtraLight.ttf new file mode 100644 index 0000000..2a2b86b Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Light.ttf new file mode 100644 index 0000000..f2560ec Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Medium.ttf new file mode 100644 index 0000000..29af14d Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Regular.ttf new file mode 100644 index 0000000..6f10212 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-SemiBold.ttf new file mode 100644 index 0000000..007c3d4 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Thin.ttf new file mode 100644 index 0000000..608d1d1 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Bold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Bold.ttf new file mode 100644 index 0000000..b5a6850 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-ExtraLight.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-ExtraLight.ttf new file mode 100644 index 0000000..3f99ab6 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-ExtraLight.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Light.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Light.ttf new file mode 100644 index 0000000..66d138b Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Medium.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Medium.ttf new file mode 100644 index 0000000..0beba47 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Regular.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Regular.ttf new file mode 100644 index 0000000..2cc8244 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-SemiBold.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-SemiBold.ttf new file mode 100644 index 0000000..67f7079 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-SemiBold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Thin.ttf b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Thin.ttf new file mode 100644 index 0000000..305c2e6 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/MaterialSymbolsSharp_Filled-Thin.ttf differ diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/BottomSafeZone.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/BottomSafeZone.kt new file mode 100644 index 0000000..0b350d2 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/BottomSafeZone.kt @@ -0,0 +1,12 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.foundations.Sizes + +@Composable +fun BottomSafeZone() { + Spacer(modifier = Modifier.height(Sizes.ScreenInset.Bottom)) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/DescriptionCard.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/DescriptionCard.kt new file mode 100644 index 0000000..b184025 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/DescriptionCard.kt @@ -0,0 +1,86 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Surface +import androidx.compose.material3.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.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import cl.homelogic.platform.designsystem.extensions.conditionalClickable +import cl.homelogic.platform.designsystem.extensions.scaledByFontSize +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes + +@Composable +fun DescriptionCard( + modifier: Modifier = Modifier, + title: String, + subtitle: String = "", + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + + Surface( + modifier = modifier + .padding(top = Padding.tiny, bottom = Padding.small) + .clip(Shapes.RoundCorner.regular) + .wrapContentSize(Alignment.TopCenter, false) + .conditionalClickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ), + shape = Shapes.RoundCorner.regular, + border = BorderStroke( + width = Sizes.Borders.Regular, + color = MaterialTheme.colorScheme.outline, + ), + ) { + Column( + modifier = Modifier.wrapContentSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + content() + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(Padding.regular) + .height(Sizes.Containers.Small.scaledByFontSize()) + .fillMaxWidth(), + ) { + Text( + text = title, + textAlign = TextAlign.Start, + style = typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = subtitle, + textAlign = TextAlign.Start, + style = typography.labelMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Divider.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Divider.kt new file mode 100644 index 0000000..7dbb6da --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Divider.kt @@ -0,0 +1,20 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.foundations.Sizes + +@Composable +fun Divider(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .height(Sizes.Stroke.Regular) + .background(MaterialTheme.colorScheme.outline), + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Markdown.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Markdown.kt new file mode 100644 index 0000000..4c94d28 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/Markdown.kt @@ -0,0 +1,856 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.mutableStateMapOf +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.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Shapes +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.gfm.GFMElementTypes +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +@Immutable +data class MarkdownTypography( + val text: TextStyle, + val code: TextStyle, + val inlineCode: TextStyle, + val h1: TextStyle, + val h2: TextStyle, + val h3: TextStyle, + val h4: TextStyle, + val h5: TextStyle, + val h6: TextStyle, + val quote: TextStyle, + val paragraph: TextStyle, + val ordered: TextStyle, + val bullet: TextStyle, + val list: TextStyle, + val textLink: TextLinkStates, + val table: TextStyle, +) + +@Immutable +data class MarkdownColors( + val codeBackground: Color, + val codeText: Color, + val dividerColor: Color, + val inlineCodeBackground: Color, + val inlineCodeText: Color, + val linkText: Color, + val tableBackground: Color, + val tableText: Color, + val text: Color, +) + +@Immutable +data class TextLinkStates( + val normal: TextStyle, + val visited: TextStyle, +) + +@Immutable +private data class MarkdownContentData( + val node: ASTNode, + val content: String, + val colors: MarkdownColors, + val typography: MarkdownTypography, + val level: Int = 0, +) + +@Composable +fun Markdown( + modifier: Modifier = Modifier, + text: String, + colors: MarkdownColors = rememberDefaultMarkdownColors(), + typography: MarkdownTypography = rememberDefaultMarkdownTypography(), +) { + val flavour = remember { GFMFlavourDescriptor() } + val parser = remember { MarkdownParser(flavour) } + + val markdownTree = remember(text) { parser.buildMarkdownTreeFromString(text) } + + val markdownContent = remember(markdownTree, text, colors, typography) { + MarkdownContentData(markdownTree, text, colors, typography) + } + + val annotatedStringCache = remember { mutableStateMapOf() } + + val listState = rememberLazyListState() + + SelectionContainer { + Column { + OptimizedMarkdownContent( + data = markdownContent, + stringCache = annotatedStringCache, + ) + } + } +} + +@Composable +private fun rememberDefaultMarkdownColors(): MarkdownColors { + val colorScheme = MaterialTheme.colorScheme + return remember { + MarkdownColors( + codeBackground = colorScheme.surfaceVariant, + codeText = colorScheme.onSurfaceVariant, + dividerColor = colorScheme.outline, + inlineCodeBackground = colorScheme.surfaceVariant, + inlineCodeText = colorScheme.onSurfaceVariant, + linkText = colorScheme.tertiary, + tableBackground = colorScheme.surface, + tableText = colorScheme.onSurface, + text = colorScheme.onPrimary, + ) + } +} + +@Composable +private fun rememberDefaultMarkdownTypography(): MarkdownTypography { + val materialTypography = MaterialTheme.typography + return remember { + MarkdownTypography( + bullet = materialTypography.bodySmall, + code = materialTypography.bodyMedium, + h1 = materialTypography.headlineLarge, + h2 = materialTypography.headlineMedium, + h3 = materialTypography.headlineSmall, + h4 = materialTypography.titleLarge, + h5 = materialTypography.titleMedium, + h6 = materialTypography.bodyLarge, + inlineCode = materialTypography.bodyLarge, + list = materialTypography.bodyMedium, + ordered = materialTypography.bodyMedium, + paragraph = materialTypography.bodyMedium, + quote = materialTypography.bodyMedium.copy( + fontStyle = FontStyle.Italic, + ), + table = materialTypography.bodySmall, + text = materialTypography.bodyMedium, + textLink = TextLinkStates( + normal = materialTypography.bodyMedium.copy( + textDecoration = TextDecoration.Underline, + ), + visited = materialTypography.bodyMedium.copy( + textDecoration = TextDecoration.Underline, + ), + ), + ) + } +} + +@Composable +private fun OptimizedMarkdownContent( + data: MarkdownContentData, + stringCache: MutableMap, + maxDepth: Int = 10, +) { + if (data.level > maxDepth) return + + for (child in data.node.children) { + val nodeKey = "${child.hashCode()}-${data.level}" + val childData = remember(child, data) { + data.copy(node = child, level = data.level + 1) + } + + when (child.type) { + MarkdownElementTypes.PARAGRAPH -> { + OptimizedParagraph(stringCache, nodeKey, child, data) + } + + MarkdownElementTypes.ATX_1, + MarkdownElementTypes.ATX_2, + MarkdownElementTypes.ATX_3, + MarkdownElementTypes.ATX_4, + MarkdownElementTypes.ATX_5, + MarkdownElementTypes.ATX_6, + -> { + OptimizedHeading( + childData = childData, + nodeKey = nodeKey, + stringCache = stringCache, + ) + } + + MarkdownElementTypes.UNORDERED_LIST -> { + OptimizedUnorderedList( + childData = childData, + stringCache = stringCache, + ) + } + + MarkdownElementTypes.ORDERED_LIST -> { + OptimizedOrderedList( + childData = childData, + stringCache = stringCache, + ) + } + + MarkdownElementTypes.BLOCK_QUOTE -> { + OptimizedBlockQuote( + childData = childData, + stringCache = stringCache, + ) + } + + MarkdownElementTypes.CODE_FENCE, + MarkdownElementTypes.CODE_BLOCK, + -> { + OptimizedCodeBlock(childData = childData) + } + + MarkdownElementTypes.HTML_BLOCK -> { + OptimizedHtmlBlock(childData = childData) + } + + GFMElementTypes.TABLE -> { + OptimizedTable( + childData = childData, + stringCache = stringCache, + ) + } + } + } +} + +@Composable +private fun OptimizedParagraph( + stringCache: MutableMap, + nodeKey: String, + child: ASTNode, + data: MarkdownContentData, +) { + val paragraphModifier = remember { + Modifier.padding( + vertical = Padding.small, + horizontal = Padding.tiny, + ) + } + + val annotatedString = stringCache.getOrPut(nodeKey) { + buildAnnotatedString { + renderInlineContent(child, data.content, data.colors, data.typography) + } + } + + Text( + text = annotatedString, + style = data.typography.paragraph, + color = data.colors.text, + modifier = paragraphModifier, + ) +} + +@Composable +private fun OptimizedHeading( + childData: MarkdownContentData, + nodeKey: String, + stringCache: MutableMap, +) { + val (style, topPadding, bottomPadding) = remember(childData.node.type) { + when (childData.node.type) { + MarkdownElementTypes.ATX_1 -> Triple( + childData.typography.h1, + Padding.regular, + Padding.small, + ) + + MarkdownElementTypes.ATX_2 -> Triple( + childData.typography.h2, + Padding.small, + Padding.small, + ) + + MarkdownElementTypes.ATX_3 -> Triple( + childData.typography.h3, + Padding.small, + Padding.small, + ) + + MarkdownElementTypes.ATX_4 -> Triple( + childData.typography.h4, + Padding.small, + Padding.tiny, + ) + + MarkdownElementTypes.ATX_5 -> Triple( + childData.typography.h5, + Padding.small, + Padding.tiny, + ) + + MarkdownElementTypes.ATX_6 -> Triple( + childData.typography.h6, + Padding.small, + Padding.tiny, + ) + + else -> Triple( + childData.typography.text, + Padding.small, + Padding.small, + ) + } + } + + val headingModifier = remember(topPadding, bottomPadding) { + Modifier.padding(top = topPadding, bottom = bottomPadding) + } + + val headingText = stringCache.getOrPut(nodeKey) { + buildAnnotatedString { + for (headerChild in childData.node.children) { + if (headerChild.type != MarkdownTokenTypes.ATX_HEADER && + headerChild.type != MarkdownTokenTypes.EOL + ) { + renderInlineContent( + headerChild, + childData.content, + childData.colors, + childData.typography, + ) + } + } + } + } + + Text( + text = headingText, + style = style, + color = childData.colors.text, + modifier = headingModifier, + ) +} + +@Composable +private fun OptimizedUnorderedList( + childData: MarkdownContentData, + stringCache: MutableMap, +) { + val listModifier = remember { + Modifier + .padding(vertical = Padding.small) + .padding(start = Padding.regular) + } + + Column(modifier = listModifier) { + for (item in childData.node.children) { + if (item.type == MarkdownElementTypes.LIST_ITEM) { + val itemData = remember(item, childData) { + childData.copy(node = item) + } + OptimizedListItem( + itemData = itemData, + stringCache = stringCache, + isOrdered = false, + index = 0, + ) + } + } + } +} + +@Composable +private fun OptimizedOrderedList( + childData: MarkdownContentData, + stringCache: MutableMap, +) { + val listModifier = remember { + Modifier + .padding(vertical = Padding.small) + .padding(start = Padding.regular) + } + + Column(modifier = listModifier) { + childData.node.children.forEachIndexed { index, item -> + if (item.type == MarkdownElementTypes.LIST_ITEM) { + val itemData = remember(item, childData) { + childData.copy(node = item) + } + OptimizedListItem( + itemData = itemData, + stringCache = stringCache, + isOrdered = true, + index = index + 1, + ) + } + } + } +} + +@Composable +private fun OptimizedListItem( + itemData: MarkdownContentData, + stringCache: MutableMap, + isOrdered: Boolean, + index: Int, +) { + val itemModifier = remember { Modifier.padding(vertical = Padding.tiny) } + + Row( + modifier = itemModifier, + verticalAlignment = Alignment.Top, + ) { + Text( + text = if (isOrdered) "$index. " else "• ", + style = if (isOrdered) itemData.typography.ordered else itemData.typography.bullet, + color = itemData.colors.text, + ) + + Column { + for (itemChild in itemData.node.children) { + val nodeKey = "${itemChild.hashCode()}-${itemData.level}" + + when (itemChild.type) { + MarkdownElementTypes.PARAGRAPH -> { + val text = stringCache.getOrPut(nodeKey) { + buildAnnotatedString { + renderInlineContent( + itemChild, + itemData.content, + itemData.colors, + itemData.typography, + ) + } + } + + Text( + text = text, + style = itemData.typography.list, + color = itemData.colors.text, + ) + } + + MarkdownElementTypes.UNORDERED_LIST, + MarkdownElementTypes.ORDERED_LIST, + -> { + val childData = remember(itemChild, itemData) { + itemData.copy(node = itemChild) + } + OptimizedMarkdownContent( + data = childData, + stringCache = stringCache, + ) + } + } + } + } + } +} + +@Composable +private fun OptimizedBlockQuote( + childData: MarkdownContentData, + stringCache: MutableMap, +) { + val blockQuoteModifier = remember { Modifier.padding(vertical = Padding.small) } + val dividerModifier = remember { + Modifier + .width(Padding.tiny) + .wrapContentHeight() + .background(childData.colors.dividerColor) + } + val contentModifier = remember { Modifier.padding(start = Padding.regular) } + + Row( + modifier = blockQuoteModifier, + verticalAlignment = Alignment.Top, + ) { + Box(modifier = dividerModifier) + + Column(modifier = contentModifier) { + for (quoteChild in childData.node.children) { + val nodeKey = "${quoteChild.hashCode()}-${childData.level}" + + if (quoteChild.type == MarkdownElementTypes.PARAGRAPH) { + val text = stringCache.getOrPut(nodeKey) { + buildAnnotatedString { + renderInlineContent( + quoteChild, + childData.content, + childData.colors, + childData.typography, + ) + } + } + + Text( + text = text, + style = childData.typography.quote, + color = childData.colors.text, + ) + } else { + val innerChildData = remember(quoteChild, childData) { + childData.copy(node = quoteChild) + } + OptimizedMarkdownContent( + data = innerChildData, + stringCache = stringCache, + ) + } + } + } + } +} + +@Composable +private fun OptimizedCodeBlock(childData: MarkdownContentData) { + val codeContent = remember(childData.node, childData.content) { + when (childData.node.type) { + MarkdownElementTypes.CODE_FENCE -> { + childData.node.children + .firstOrNull { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT } + ?.getTextInNode(childData.content) + ?.toString() ?: "" + } + + MarkdownElementTypes.CODE_BLOCK -> { + childData.node.children + .firstOrNull { it.type == MarkdownTokenTypes.CODE_LINE } + ?.getTextInNode(childData.content) + ?.toString() ?: "" + } + + else -> "" + } + } + + val codeBlockModifier = remember { + Modifier + .clip(Shapes.RoundCorner.regular) + .background(childData.colors.codeBackground) + .fillMaxWidth() + .padding(vertical = Padding.small) + } + + val textModifier = remember { Modifier.padding(Padding.regular) } + + Surface( + modifier = codeBlockModifier, + color = childData.colors.codeBackground, + ) { + Text( + text = codeContent, + style = childData.typography.code, + color = childData.colors.codeText, + modifier = textModifier, + ) + } +} + +@Composable +private fun OptimizedHtmlBlock(childData: MarkdownContentData) { + val htmlContent = remember(childData.node, childData.content) { + childData.node.getTextInNode(childData.content).toString() + } + + val htmlBlockModifier = remember { Modifier.padding(vertical = Padding.small) } + + Text( + text = htmlContent, + style = childData.typography.text, + color = childData.colors.text, + modifier = htmlBlockModifier, + ) +} + +@Composable +private fun OptimizedTable( + childData: MarkdownContentData, + stringCache: MutableMap, +) { + val tableModifier = remember { + Modifier + .fillMaxWidth() + .padding(vertical = Padding.small) + } + + val tableContentModifier = remember { Modifier.padding(Padding.small) } + + Surface( + color = childData.colors.tableBackground, + modifier = tableModifier, + ) { + Column(modifier = tableContentModifier) { + for (row in childData.node.children) { + if (row.type == GFMElementTypes.HEADER || + row.type == GFMElementTypes.ROW + ) { + val rowData = remember(row, childData) { + childData.copy(node = row) + } + OptimizedTableRow( + rowData = rowData, + stringCache = stringCache, + ) + + if (row.type == GFMElementTypes.HEADER) { + Divider(modifier = Modifier.padding(bottom = Padding.small)) + } + } + } + } + } +} + +@Composable +private fun OptimizedTableRow( + rowData: MarkdownContentData, + stringCache: MutableMap, +) { + val rowModifier = remember { Modifier.fillMaxWidth() } + + val cellNodes = remember(rowData.node) { + rowData.node.children.filter { + it.type != GFMElementTypes.HEADER && + it.type != GFMElementTypes.ROW && + it.type != GFMElementTypes.TABLE + } + } + + Row( + modifier = rowModifier, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + cellNodes.forEach { cellNode -> + val cellData = remember(cellNode, rowData) { + rowData.copy(node = cellNode) + } + + val isHeader = remember(rowData.node.type) { + rowData.node.type == GFMElementTypes.HEADER + } + + OptimizedTableCell( + modifier = Modifier.weight(1f), + cellData = cellData, + stringCache = stringCache, + isHeader = isHeader, + ) + } + } +} + +@Composable +private fun OptimizedTableCell( + modifier: Modifier = Modifier, + cellData: MarkdownContentData, + stringCache: MutableMap, + isHeader: Boolean, +) { + val nodeKey = "${cellData.node.hashCode()}-${cellData.level}" + val cellModifier = remember { modifier.padding(Padding.tiny) } + + val cellText = stringCache.getOrPut(nodeKey) { + buildAnnotatedString { + renderInlineContent( + cellData.node, + cellData.content, + cellData.colors, + cellData.typography, + ) + } + } + + val style = remember(isHeader) { + if (isHeader) cellData.typography.h4 else cellData.typography.table + } + + val fontWeight = remember(isHeader) { + if (isHeader) FontWeight.Bold else FontWeight.Normal + } + + Text( + text = cellText, + style = style, + color = cellData.colors.tableText, + fontWeight = fontWeight, + modifier = cellModifier, + ) +} + +private fun AnnotatedString.Builder.renderInlineContent( + node: ASTNode, + content: String, + colors: MarkdownColors, + typography: MarkdownTypography, +) { + for (child in node.children) { + when (child.type) { + MarkdownTokenTypes.TEXT, MarkdownTokenTypes.WHITE_SPACE -> { + append(child.getTextInNode(content)) + } + + MarkdownElementTypes.EMPH -> { + renderEmphasis(child, content, colors, typography) + } + + MarkdownElementTypes.STRONG -> { + renderStrong(child, content, colors, typography) + } + + MarkdownElementTypes.CODE_SPAN -> { + renderCodeSpan(child, content, colors) + } + + MarkdownElementTypes.INLINE_LINK -> { + renderInlineLink(child, content, colors, typography) + } + + MarkdownElementTypes.FULL_REFERENCE_LINK, + MarkdownElementTypes.SHORT_REFERENCE_LINK, + -> { + renderReferenceLink(child, content, colors, typography) + } + + MarkdownTokenTypes.HARD_LINE_BREAK -> { + append("\n") + } + + else -> { + renderInlineContent(child, content, colors, typography) + } + } + } +} + +private fun AnnotatedString.Builder.renderEmphasis( + node: ASTNode, + content: String, + colors: MarkdownColors, + typography: MarkdownTypography, +) { + pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) + renderInlineContent(node, content, colors, typography) + pop() +} + +private fun AnnotatedString.Builder.renderStrong( + node: ASTNode, + content: String, + colors: MarkdownColors, + typography: MarkdownTypography, +) { + pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) + renderInlineContent(node, content, colors, typography) + pop() +} + +private fun AnnotatedString.Builder.renderCodeSpan( + node: ASTNode, + content: String, + colors: MarkdownColors, +) { + pushStyle( + SpanStyle( + background = colors.inlineCodeBackground, + color = colors.inlineCodeText, + fontFamily = FontFamily.Monospace, + ), + ) + append("${node.getTextInNode(content).trim('`')}") + pop() +} + +private fun AnnotatedString.Builder.renderInlineLink( + node: ASTNode, + content: String, + colors: MarkdownColors, + typography: MarkdownTypography, +) { + val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT } + val linkDestination = + node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION } + val url = linkDestination?.getTextInNode(content)?.trim('(', ')')?.toString() ?: "" + + val startPosition = length + + pushStyle(SpanStyle(color = colors.linkText)) + + if (linkText != null) { + val linkTextContent = linkText.getTextInNode(content) + if (linkTextContent.isNotEmpty()) { + append(linkTextContent) + } else { + for (textChild in linkText.children) { + if (textChild.type != MarkdownTokenTypes.LBRACKET && + textChild.type != MarkdownTokenTypes.RBRACKET + ) { + renderInlineContent(textChild, content, colors, typography) + } + } + } + } else { + append(url) + } + + if (length > startPosition) { + addLink( + url = LinkAnnotation.Url(url = url), + start = startPosition, + end = length, + ) + } + + pop() +} + +private fun AnnotatedString.Builder.renderReferenceLink( + node: ASTNode, + content: String, + colors: MarkdownColors, + typography: MarkdownTypography, +) { + val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT } + + pushStyle( + SpanStyle( + color = colors.linkText, + textDecoration = TextDecoration.Underline, + ), + ) + + if (linkText != null) { + for (textChild in linkText.children) { + if (textChild.type != MarkdownTokenTypes.LBRACKET && + textChild.type != MarkdownTokenTypes.RBRACKET + ) { + renderInlineContent(textChild, content, colors, typography) + } + } + } + + pop() +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SectionTitle.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SectionTitle.kt new file mode 100644 index 0000000..8845581 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SectionTitle.kt @@ -0,0 +1,36 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes + +@Composable +fun SectionTitle( + text: String, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.BottomStart, + modifier = modifier + .fillMaxWidth() + .height(Sizes.Containers.Small) + .padding(bottom = Padding.small), + ) { + Box { + Text( + text = text, + style = typography.headlineMedium, + overflow = TextOverflow.Visible, + ) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SegmentedControl.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SegmentedControl.kt new file mode 100644 index 0000000..19d7034 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SegmentedControl.kt @@ -0,0 +1,145 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes +import kotlin.math.abs +import kotlin.math.roundToInt + +@Composable +fun SegmentedControl( + modifier: Modifier = Modifier, + items: List, + initialSelectedIndex: Int, + onSelectedIndexChange: (Int) -> Unit, +) { + var selectedIndex by remember { mutableIntStateOf(initialSelectedIndex) } + + val density = LocalDensity.current + val haptic = LocalHapticManager.current + var componentWidth by remember { mutableStateOf(0) } + val itemWidth = remember(componentWidth) { componentWidth.toFloat() / items.size } + val pillOffset by animateFloatAsState( + targetValue = itemWidth * selectedIndex, + animationSpec = spring( + dampingRatio = 0.83f, + stiffness = Spring.StiffnessMediumLow, + ), + label = "SegmentedControl-pillOffset", + ) + + Box( + modifier = modifier + .height(Sizes.Containers.Small) + .clip(Shapes.RoundCorner.regular) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(Padding.tiny) + .onSizeChanged { size -> + if (componentWidth != size.width) { + componentWidth = size.width + } + }, + ) { + // Animated Pill + Box( + modifier = Modifier + .offset { IntOffset(pillOffset.roundToInt(), 0) } + .width(with(density) { itemWidth.toDp() }) + .fillMaxHeight() + .shadow(Sizes.Shadows.Regular, shape = Shapes.RoundCorner.small) + .clip(Shapes.RoundCorner.small) + .background(MaterialTheme.colorScheme.primaryContainer), + ) + + SegmentedControlItems(items, selectedIndex) { newIndex -> + if (newIndex != selectedIndex) { + // Do a stronger vibration if the pill moves more than + val difference = abs(newIndex - selectedIndex) + if (difference < 3) { + haptic.performFeedback(FeedbackType.SegmentTick) + } else { + haptic.performFeedback(FeedbackType.LongPress) + } + } + selectedIndex = newIndex + onSelectedIndexChange(newIndex) + } + } +} + +@Composable +private fun SegmentedControlItems( + items: List, + selectedIndex: Int, + onSelectedIndexChange: (Int) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + items.forEachIndexed { index, item -> + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + onSelectedIndexChange(index) + }.padding(horizontal = Padding.small), + contentAlignment = Alignment.Center, + ) { + Text( + text = item, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (selectedIndex == index) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + style = if (selectedIndex == index) { + typography.bodyLarge + } else { + typography.bodyMedium + }, + ) + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleButton.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleButton.kt new file mode 100644 index 0000000..211eef2 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleButton.kt @@ -0,0 +1,83 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import cl.homelogic.platform.designsystem.extensions.clickable +import cl.homelogic.platform.designsystem.extensions.scaledByFontSize +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes +import cl.homelogic.platform.designsystem.foundations.Symbols + +enum class ButtonStyle { + PRIMARY, + SECONDARY, + TERTIARY, +} + +@Composable +fun SimpleButton( + modifier: Modifier = Modifier, + style: ButtonStyle = ButtonStyle.PRIMARY, + text: String = "", + leadingSymbol: Symbols? = null, + enabled: Boolean = true, + hapticFeedback: Boolean = true, + onClick: () -> Unit, +) { + val haptic = LocalHapticManager.current + + val (background, textColor) = when (style) { + ButtonStyle.PRIMARY -> { + MaterialTheme.colorScheme.tertiary to MaterialTheme.colorScheme.onTertiary + } + + ButtonStyle.SECONDARY -> { + MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onSurface + } + + ButtonStyle.TERTIARY -> { + Color.Transparent to MaterialTheme.colorScheme.onSurface + } + } + + Button( + onClick = { + if (hapticFeedback) haptic.performFeedback(FeedbackType.SegmentTick) + onClick() + }, + shape = Shapes.RoundCorner.big, + enabled = enabled, + colors = ButtonDefaults.buttonColors().copy( + containerColor = background, + contentColor = textColor, + disabledContainerColor = background, + disabledContentColor = textColor, + ), + modifier = modifier + .fillMaxWidth() + .height(Sizes.Buttons.Regular.scaledByFontSize()) + .clickable(enabled), + ) { + SymbolText( + modifier = Modifier.padding(end = Padding.small), + color = textColor, + icon = leadingSymbol, + ) + Text( + text = text, + style = typography.bodyLarge, + ) + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCard.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCard.kt new file mode 100644 index 0000000..75b7bbc --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCard.kt @@ -0,0 +1,45 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import cl.homelogic.platform.designsystem.extensions.conditionalClickable +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes + +@Composable +fun SimpleCard( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + + Surface( + modifier = modifier + .wrapContentSize(Alignment.TopCenter, false) + .clip(Shapes.RoundCorner.regular) + .fillMaxWidth() + .conditionalClickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ), + shape = Shapes.RoundCorner.regular, + border = BorderStroke( + width = Sizes.Borders.Regular, + color = MaterialTheme.colorScheme.outline, + ), + ) { + content() + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCheckbox.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCheckbox.kt new file mode 100644 index 0000000..b2850cd --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleCheckbox.kt @@ -0,0 +1,42 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.extensions.clickable +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager + +@Composable +fun SimpleCheckbox( + checked: Boolean = false, + onCheckedChange: ((Boolean) -> Unit)? = {}, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + val haptic = LocalHapticManager.current + + Checkbox( + checked = checked, + onCheckedChange = { + if (it) { + haptic.performFeedback(FeedbackType.ToggleOn) + } else { + haptic.performFeedback(FeedbackType.ToggleOff) + } + onCheckedChange?.invoke(it) + }, + modifier = modifier.clickable(enabled), + enabled = enabled, + colors = CheckboxDefaults.colors().copy( + checkedCheckmarkColor = MaterialTheme.colorScheme.onTertiary, + checkedBoxColor = MaterialTheme.colorScheme.tertiary, + checkedBorderColor = MaterialTheme.colorScheme.tertiary, + uncheckedBorderColor = MaterialTheme.colorScheme.outline, + disabledBorderColor = MaterialTheme.colorScheme.tertiary, + disabledCheckedBoxColor = MaterialTheme.colorScheme.tertiary, + ), + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleWindow.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleWindow.kt new file mode 100644 index 0000000..5fcca32 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SimpleWindow.kt @@ -0,0 +1,54 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import cl.homelogic.platform.designsystem.foundations.Dimens +import cl.homelogic.platform.designsystem.foundations.Padding + +@Composable +fun SimpleWindow( + modifier: Modifier = Modifier.fillMaxSize(), + unbounded: Boolean = false, + topBar: @Composable () -> Unit = {}, + content: @Composable ColumnScope.() -> Unit, +) { + val imeVisible by rememberUpdatedState( + WindowInsets.ime.getBottom(LocalDensity.current) > 0, + ) + + Scaffold( + topBar = topBar, + contentWindowInsets = if (!imeVisible) { + WindowInsets( + left = Dimens.at0, + top = Dimens.at0, + right = Dimens.at0, + bottom = Dimens.at0, + ) + } else { + WindowInsets.ime + }, + modifier = modifier, + ) { padding -> + Surface(modifier = Modifier.padding(padding)) { + Column( + modifier = Modifier.padding(horizontal = Padding.big).takeIf { !unbounded } + ?: Modifier, + ) { + content() + BottomSafeZone() + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SymbolText.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SymbolText.kt new file mode 100644 index 0000000..285609f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/atoms/SymbolText.kt @@ -0,0 +1,26 @@ +package cl.homelogic.platform.designsystem.components.atoms + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import cl.homelogic.platform.designsystem.foundations.Symbols + +@Composable +fun SymbolText( + icon: Symbols? = null, + rawIcon: String = "", + style: TextStyle = typography.displaySmall, + color: Color = MaterialTheme.colorScheme.primary, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = icon?.codePoint ?: rawIcon, + color = color, + style = style, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/IconShortcut.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/IconShortcut.kt new file mode 100644 index 0000000..efff13f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/IconShortcut.kt @@ -0,0 +1,78 @@ +package cl.homelogic.platform.designsystem.components.molecules + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.components.atoms.SimpleCard +import cl.homelogic.platform.designsystem.components.atoms.SymbolText +import cl.homelogic.platform.designsystem.extensions.scaledByFontSize +import cl.homelogic.platform.designsystem.foundations.Dimens +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes +import cl.homelogic.platform.designsystem.foundations.Symbols + +@Composable +fun IconShortcut( + modifier: Modifier = Modifier, + icon: Symbols? = null, + rawIcon: String = "", + title: String, + hasNotification: Boolean = false, + onClick: (() -> Unit)? = null, +) { + val haptic = LocalHapticManager.current + + SimpleCard( + onClick = { + haptic.performFeedback(FeedbackType.SegmentTick) + onClick?.invoke() + }, + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(Sizes.Buttons.Big.scaledByFontSize()), + ) { + Row( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(start = Padding.small), + verticalAlignment = Alignment.CenterVertically, + ) { + icon?.let { SymbolText(icon = icon) } ?: SymbolText(rawIcon = rawIcon) + Spacer(modifier = Modifier.width(Sizes.Spacers.Regular)) + Text( + text = title, + style = typography.labelLarge, + ) + } + + if (hasNotification) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(Dimens.at8) + .align(Alignment.TopEnd) + .background(MaterialTheme.colorScheme.tertiary), + ) + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/LazySectionedList.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/LazySectionedList.kt new file mode 100644 index 0000000..c31a663 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/LazySectionedList.kt @@ -0,0 +1,62 @@ +package cl.homelogic.platform.designsystem.components.molecules + +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.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.components.atoms.BottomSafeZone +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes + +data class SectionedListItem( + val title: String, + val composables: List<@Composable () -> Unit>, +) + +@Composable +fun LazySectionedList( + modifier: Modifier = Modifier, + items: List, + addSeparator: Boolean, +) { + LazyColumn( + modifier = modifier, + ) { + items(items) { sizeGroup -> + Section(sizeGroup.title) { + Column(modifier = Modifier.fillMaxWidth()) { + sizeGroup.composables.forEachIndexed { index, item -> + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = Padding.small), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + item() + } + if (index < sizeGroup.composables.size - 1 && addSeparator) { + HorizontalDivider( + thickness = Sizes.Stroke.Regular, + color = MaterialTheme.colorScheme.outline, + ) + } + } + } + } + } + item { + BottomSafeZone() + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/OnboardingWindow.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/OnboardingWindow.kt new file mode 100644 index 0000000..4ec02d8 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/OnboardingWindow.kt @@ -0,0 +1,207 @@ +package cl.homelogic.platform.designsystem.components.molecules + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import cl.homelogic.platform.designsystem.components.atoms.BottomSafeZone +import cl.homelogic.platform.designsystem.components.atoms.ButtonStyle +import cl.homelogic.platform.designsystem.components.atoms.SimpleButton +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes +import kotlinx.coroutines.launch + +class OnboardingNavigationContext( + val currentPage: Int, + val pageCount: Int, + private val navigateTo: suspend (Int) -> Unit, + private val restrictNavigation: (Boolean) -> Unit, +) { + val isFirstPage: Boolean get() = currentPage == 0 + val isLastPage: Boolean get() = currentPage == pageCount - 1 + + suspend fun goToNextPage() { + if (currentPage < pageCount - 1) { + navigateTo(currentPage + 1) + } + } + + suspend fun goToPreviousPage() { + if (currentPage > 0) { + navigateTo(currentPage - 1) + } + } + + fun enableNavigation() { + restrictNavigation(false) + } +} + +data class OnboardingSlide( + val restrictNavigation: Boolean = false, + val content: @Composable OnboardingNavigationContext.() -> Unit, +) + +@Composable +fun OnboardingWindow( + modifier: Modifier = Modifier, + slides: List, + continueButtonText: String, + showIndicators: Boolean = true, + onFinish: () -> Unit = {}, +) { + if (slides.isEmpty()) { + onFinish() + } + + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState(pageCount = { slides.size }) + + val isLastPage = pagerState.currentPage == slides.size - 1 + val currentSlide = slides[pagerState.currentPage] + + var hasRestrictedNavigation by remember(pagerState.currentPage) { + mutableStateOf(currentSlide.restrictNavigation) + } + + val navigationContext = remember(pagerState.currentPage, hasRestrictedNavigation) { + OnboardingNavigationContext( + currentPage = pagerState.currentPage, + pageCount = slides.size, + navigateTo = { page -> pagerState.animateScrollToPage(page) }, + restrictNavigation = { hasRestrictedNavigation = it }, + ) + } + + Box( + modifier = modifier + .fillMaxSize() + .pointerInput(pagerState.currentPage, hasRestrictedNavigation) { + if (!hasRestrictedNavigation) { + detectHorizontalDragGestures( + onDragStart = {}, + onDragEnd = {}, + onDragCancel = {}, + onHorizontalDrag = { change, dragAmount -> + change.consume() + + if (dragAmount > 50 && !navigationContext.isFirstPage) { + coroutineScope.launch { + navigationContext.goToPreviousPage() + } + } else if (dragAmount < -50 && !navigationContext.isLastPage) { + coroutineScope.launch { + navigationContext.goToNextPage() + } + } + }, + ) + } + }, + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .weight(1f) + .wrapContentHeight() + .fillMaxWidth(), + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + userScrollEnabled = !hasRestrictedNavigation, + ) { page -> + val slideNavigationContext = remember(page, pagerState.currentPage) { + OnboardingNavigationContext( + currentPage = page, + pageCount = slides.size, + navigateTo = { targetPage -> pagerState.animateScrollToPage(targetPage) }, + restrictNavigation = { + if (page == pagerState.currentPage) { + hasRestrictedNavigation = it + } + }, + ) + } + + slides[page].content(slideNavigationContext) + } + } + + if (showIndicators) { + PageIndicators( + pageCount = slides.size, + currentPage = pagerState.currentPage, + modifier = Modifier.padding(Padding.big), + ) + } + + SimpleButton( + modifier = Modifier + .fillMaxWidth() + .padding(Padding.big), + style = ButtonStyle.TERTIARY, + text = continueButtonText, + enabled = !hasRestrictedNavigation, + ) { + coroutineScope.launch { + if (isLastPage) { + onFinish() + } else { + navigationContext.goToNextPage() + } + } + } + + BottomSafeZone() + } + } +} + +@Composable +private fun PageIndicators( + pageCount: Int, + currentPage: Int, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = modifier, + ) { + repeat(pageCount) { index -> + val isSelected = index == currentPage + Box( + modifier = Modifier + .padding(horizontal = Padding.tiny) + .size(if (isSelected) Sizes.Dots.Regular else Sizes.Dots.Regular) + .background( + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, + shape = CircleShape, + ), + ) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/RadioButtonSelector.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/RadioButtonSelector.kt new file mode 100644 index 0000000..165660e --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/RadioButtonSelector.kt @@ -0,0 +1,87 @@ +package cl.homelogic.platform.designsystem.components.molecules + +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.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.components.atoms.SimpleCard +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes + +data class RadioButtonItem( + val text: String, + val data: T, +) + +@Composable +fun RadioButtonSelector( + items: List>, + initialSelection: RadioButtonItem? = null, + onItemSelected: (T) -> Unit, + modifier: Modifier = Modifier, +) { + var selectedItem by remember { mutableStateOf(initialSelection) } + val haptic = LocalHapticManager.current + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Sizes.Spacers.Regular), + ) { + items.forEach { item -> + SelectorOption( + item = item, + isSelected = item == selectedItem, + onSelect = { + haptic.performFeedback(FeedbackType.SegmentTick) + selectedItem = item + onItemSelected(item.data) + }, + ) + } + } +} + +@Composable +private fun SelectorOption( + item: RadioButtonItem, + isSelected: Boolean, + onSelect: () -> Unit, +) { + SimpleCard( + onClick = onSelect, + modifier = Modifier.wrapContentHeight(), + ) { + Row( + modifier = Modifier + .padding(Padding.small) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = Padding.small), + text = item.text, + style = MaterialTheme.typography.bodyMedium, + ) + + RadioButton( + selected = isSelected, + onClick = onSelect, + ) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SearchBar.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SearchBar.kt new file mode 100644 index 0000000..c078947 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SearchBar.kt @@ -0,0 +1,113 @@ +package cl.homelogic.platform.designsystem.components.molecules + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import cl.homelogic.platform.designsystem.components.atoms.SymbolText +import cl.homelogic.platform.designsystem.extensions.intangible +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes +import cl.homelogic.platform.designsystem.foundations.Symbols + +@Composable +fun SearchBar( + modifier: Modifier = Modifier, + hint: String = "", + onTextChange: (String) -> Unit = {}, + onSearch: (String) -> Unit = {}, +) { + var searchText by remember { mutableStateOf("") } + var isFocused by remember { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + Box( + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = Sizes.TextFields.Regular) + .clip(Shapes.RoundCorner.regular) + .background(MaterialTheme.colorScheme.surfaceVariant) + .clickable { + focusRequester.requestFocus() + }.padding(horizontal = Padding.regular), + contentAlignment = Alignment.CenterStart, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + SymbolText( + icon = Symbols.IconSearch, + style = MaterialTheme.typography.displayMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.width(Sizes.Spacers.Regular)) + + BasicTextField( + value = searchText, + onValueChange = { newText -> + searchText = newText + onTextChange(newText) + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Padding.regular) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + }, + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search, + ), + keyboardActions = KeyboardActions( + onSearch = { + onSearch(searchText) + focusManager.clearFocus() + }, + ), + decorationBox = { innerTextField -> + if (searchText.isEmpty() && !isFocused) { + Text( + text = hint, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.intangible(), + ) + } else { + innerTextField() + } + }, + ) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/Section.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/Section.kt new file mode 100644 index 0000000..277ec62 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/Section.kt @@ -0,0 +1,15 @@ +package cl.homelogic.platform.designsystem.components.molecules + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cl.homelogic.platform.designsystem.components.atoms.SectionTitle + +@Composable +fun Section( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + SectionTitle(title, modifier) + content() +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SymbolButton.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SymbolButton.kt new file mode 100644 index 0000000..729c7e5 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/molecules/SymbolButton.kt @@ -0,0 +1,51 @@ +package cl.homelogic.platform.designsystem.components.molecules + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.runtime.Composable +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 cl.homelogic.platform.designsystem.components.atoms.SymbolText +import cl.homelogic.platform.designsystem.foundations.FeedbackType +import cl.homelogic.platform.designsystem.foundations.LocalHapticManager +import cl.homelogic.platform.designsystem.foundations.Shapes +import cl.homelogic.platform.designsystem.foundations.Sizes +import cl.homelogic.platform.designsystem.foundations.Symbols + +@Composable +fun SymbolButton( + modifier: Modifier = Modifier.size(Sizes.Buttons.Regular), + icon: Symbols, + color: Color = MaterialTheme.colorScheme.primary, + background: Color = Color.Transparent, + hapticFeedback: Boolean = true, + onClick: (() -> Unit)? = null, +) { + val haptic = LocalHapticManager.current + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .clip(Shapes.RoundCorner.regular) + .background(background) + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = { + if (hapticFeedback) haptic.performFeedback(FeedbackType.SegmentTick) + onClick?.invoke() + }, + ), + contentAlignment = Alignment.Center, + ) { + SymbolText(icon = icon, style = typography.displaySmall, color = color) + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/organisms/Header.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/organisms/Header.kt new file mode 100644 index 0000000..e13621b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/organisms/Header.kt @@ -0,0 +1,213 @@ +package cl.homelogic.platform.designsystem.components.organisms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import cl.homelogic.platform.designsystem.components.molecules.SymbolButton +import cl.homelogic.platform.designsystem.extensions.requirePrecondition +import cl.homelogic.platform.designsystem.foundations.Padding +import cl.homelogic.platform.designsystem.foundations.Sizes +import cl.homelogic.platform.designsystem.foundations.Symbols + +sealed class HeaderTypes { + data class Home( + val title: String, + val onSettings: () -> Unit, + ) : HeaderTypes() + + data class SubHome( + val title: String, + ) : HeaderTypes() + + data class Step( + val title: String, + val onBack: () -> Unit, + ) : HeaderTypes() + + data class Modal( + val onClosed: () -> Unit, + ) : HeaderTypes() + + data class Flex( + val title: String, + val leftShortcuts: List, + val rightShortcuts: List, + ) : HeaderTypes() { + init { + requirePrecondition(leftShortcuts.size <= 3) + requirePrecondition(rightShortcuts.size <= 3) + } + } +} + +data class FlexHeaderShortcut( + val icon: Symbols, + val onClick: () -> Unit, +) + +@Composable +fun Header( + type: HeaderTypes, + showDivider: Boolean = true, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .height(Sizes.Header.Regular), + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (type) { + is HeaderTypes.Home -> buildHomeHeader(type) + is HeaderTypes.SubHome -> buildSubHomeHeader(type) + is HeaderTypes.Step -> buildFlowHeader(type) + is HeaderTypes.Modal -> buildModalHeader(type) + is HeaderTypes.Flex -> buildFlexHeader(type) + } + + if (showDivider) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(Sizes.Stroke.Regular) + .background(MaterialTheme.colorScheme.outline) + .align(Alignment.BottomStart), + ) + } + } + } +} + +@Composable +private fun buildHomeHeader(type: HeaderTypes.Home) { + Text( + text = type.title, + textAlign = TextAlign.Center, + style = typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Clip, + ) + + Row( + modifier = Modifier + .fillMaxSize() + .padding(Padding.small), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + SymbolButton( + icon = Symbols.IconSettings, + onClick = type.onSettings, + ) + } +} + +@Composable +private fun buildSubHomeHeader(type: HeaderTypes.SubHome) { + Text( + text = type.title, + textAlign = TextAlign.Center, + style = typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Clip, + ) +} + +@Composable +private fun buildFlowHeader(type: HeaderTypes.Step) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(Padding.small), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + SymbolButton( + icon = Symbols.IconArrowBack, + onClick = type.onBack, + ) + } + + Text( + text = type.title, + textAlign = TextAlign.Center, + style = typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Clip, + ) +} + +@Composable +private fun buildModalHeader(type: HeaderTypes.Modal) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(Padding.small), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + SymbolButton( + icon = Symbols.IconClose, + onClick = type.onClosed, + ) + } +} + +@Composable +private fun buildFlexHeader(type: HeaderTypes.Flex) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(Padding.small), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + type.leftShortcuts.forEach { shortcut -> + SymbolButton( + icon = shortcut.icon, + onClick = shortcut.onClick, + ) + } + } + + Text( + text = type.title, + textAlign = TextAlign.Center, + style = typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Clip, + ) + + Row( + modifier = Modifier + .fillMaxSize() + .padding(Padding.small), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + type.rightShortcuts.forEach { shortcut -> + SymbolButton( + icon = shortcut.icon, + onClick = shortcut.onClick, + ) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Chart.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Chart.kt new file mode 100644 index 0000000..6d1e265 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Chart.kt @@ -0,0 +1,58 @@ +package cl.homelogic.platform.designsystem.components.zExperiments + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +@Composable +fun TemperatureGraph( + data: List, + modifier: Modifier = Modifier, + lineColor: Color = Color(0xFF6B7280), +) { + Canvas(modifier = modifier) { + if (data.isEmpty()) return@Canvas + + val path = Path() + val points = Path() + + val xStep = size.width / (data.size - 1) + val yStep = size.height / (data.maxOrNull() ?: 1f) + + // Create smooth curve + path.moveTo(0f, size.height - (data.first() * yStep)) + points.moveTo(0f, size.height - (data.first() * yStep)) + + data.forEachIndexed { index, value -> + if (index == 0) return@forEachIndexed + val x = index * xStep + val y = size.height - (value * yStep) + + // Create smooth curve + val controlX1 = ((index - 1) * xStep + x) / 2f + val controlX2 = controlX1 + val controlY1 = size.height - (data[index - 1] * yStep) + val controlY2 = y + + path.cubicTo(controlX1, controlY1, controlX2, controlY2, x, y) + points.cubicTo(controlX1, controlY1, controlX2, controlY2, x, y) + } + + // Draw the line + drawPath( + path = path, + color = lineColor, + style = Stroke( + width = 2.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round, + ), + ) + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Greeting.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Greeting.kt new file mode 100644 index 0000000..5cd72a1 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/Greeting.kt @@ -0,0 +1,9 @@ +package cl.homelogic.platform.designsystem.components.zExperiments + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun Greeting(name: String) { + Text(text = "Hello, $name!") +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/TemperatureChart.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/TemperatureChart.kt new file mode 100644 index 0000000..0a1f76f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/components/zExperiments/TemperatureChart.kt @@ -0,0 +1,95 @@ +package cl.homelogic.platform.designsystem.components.zExperiments + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import cl.homelogic.platform.designsystem.foundations.Shapes + +@Composable +fun TemperatureCard( + modifier: Modifier = Modifier, + temperature: Int = 72, + percentage: Float = 2f, + temperatureData: List = emptyList(), +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + shape = Shapes.RoundCorner.regular, + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + ) { + Column( + modifier = Modifier + .padding(24.dp) + .fillMaxWidth(), + ) { + Text( + text = "Temperature", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = temperature.toString(), + style = MaterialTheme.typography.displayLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Now", + style = MaterialTheme.typography.bodyLarge, + color = Color.Gray, + ) + Text( + text = "+${percentage.toInt()}%", + style = MaterialTheme.typography.bodyLarge, + color = Color(0xFF22C55E), // Green color + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + TemperatureGraph( + data = temperatureData, + modifier = Modifier + .fillMaxWidth() + .height(120.dp), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text("9AM", color = Color.Gray) + Text("12PM", color = Color.Gray) + Text("3PM", color = Color.Gray) + Text("6PM", color = Color.Gray) + Text("9PM", color = Color.Gray) + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/AccessibilityHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/AccessibilityHelpers.kt new file mode 100644 index 0000000..6b5ea3a --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/AccessibilityHelpers.kt @@ -0,0 +1,29 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.testTag + +data class AccessibilityInfo( + val testTag: String, + val contentDescription: String, + val role: Role, + val stateDescription: String? = null, +) + +fun Modifier.accessibility( + accessibilityInfo: AccessibilityInfo, + mergeDescendants: Boolean = true, +) = this.semantics(mergeDescendants) { + testTag = accessibilityInfo.testTag + contentDescription = accessibilityInfo.contentDescription + role = accessibilityInfo.role + + accessibilityInfo.stateDescription?.let { + stateDescription = it + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ClassHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ClassHelpers.kt new file mode 100644 index 0000000..651669b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ClassHelpers.kt @@ -0,0 +1,13 @@ +package cl.homelogic.platform.designsystem.extensions + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +fun requirePrecondition(value: Boolean) { + contract { returns() implies value } + if (!value) { + throw IllegalArgumentException("Shortcuts cannot exceed 3 items") + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ColorHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ColorHelpers.kt new file mode 100644 index 0000000..a7130a8 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ColorHelpers.kt @@ -0,0 +1,15 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.ui.graphics.Color +import kotlin.math.roundToInt + +fun Color.toHexString(): String { + val red = (this.red * 255).roundToInt() + val green = (this.green * 255).roundToInt() + val blue = (this.blue * 255).roundToInt() + + return "#" + red.toHexString() + + green.toHexString() + blue.toHexString() +} + +private fun Int.toHexString(): String = this.toString(16).padStart(2, '0').uppercase() diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/FontHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/FontHelpers.kt new file mode 100644 index 0000000..2b79399 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/FontHelpers.kt @@ -0,0 +1,31 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.ui.text.font.FontWeight + +enum class FontWeightTypes( + val weight: Int, +) { + Thin(100), + ExtraLight(200), + Light(300), + Normal(400), + Medium(500), + SemiBold(600), + Bold(700), + ExtraBold(800), + Black(900), +} + +fun FontWeight.toType(): FontWeightTypes = + when (this.weight) { + FontWeightTypes.Thin.weight -> FontWeightTypes.Thin + FontWeightTypes.ExtraLight.weight -> FontWeightTypes.ExtraLight + FontWeightTypes.Light.weight -> FontWeightTypes.Light + FontWeightTypes.Normal.weight -> FontWeightTypes.Normal + FontWeightTypes.Medium.weight -> FontWeightTypes.Medium + FontWeightTypes.SemiBold.weight -> FontWeightTypes.SemiBold + FontWeightTypes.Bold.weight -> FontWeightTypes.Bold + FontWeightTypes.ExtraBold.weight -> FontWeightTypes.ExtraBold + FontWeightTypes.Black.weight -> FontWeightTypes.Black + else -> FontWeightTypes.Normal + } diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/IconHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/IconHelpers.kt new file mode 100644 index 0000000..1c0bdfb --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/IconHelpers.kt @@ -0,0 +1,10 @@ +package cl.homelogic.platform.designsystem.extensions + +fun String.toUnicode(): String? = + try { + val paddedHex = this.padStart(4, '0') + val codePoint = paddedHex.toInt(16) + codePoint.toChar().toString() + } catch (e: Exception) { + null + } diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ListHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ListHelpers.kt new file mode 100644 index 0000000..d6722ac --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ListHelpers.kt @@ -0,0 +1,5 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.runtime.Composable + +fun listOfComposables(vararg composables: @Composable () -> Unit): List<@Composable () -> Unit> = composables.toList() diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ModifierHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ModifierHelpers.kt new file mode 100644 index 0000000..8f885bd --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/ModifierHelpers.kt @@ -0,0 +1,57 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics + +fun Modifier.disable(): Modifier = + this + .alpha(0.3f) + .clickable(enabled = false) {} + .focusable(false) + .semantics { + disabled() + }.pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + event.changes.forEach { pointerInputChange: PointerInputChange -> + pointerInputChange.consume() + } + } + } + } + +fun Modifier.clickable(status: Boolean): Modifier = + if (status) { + this + } else { + this.disable() + } + +fun Modifier.intangible(): Modifier = + this + .alpha(0.3f) + +fun Modifier.conditionalClickable( + onClick: (() -> Unit)?, + interactionSource: MutableInteractionSource, + indication: Indication?, +): Modifier = + if (onClick != null) { + this.clickable( + interactionSource = interactionSource, + indication = indication, + onClick = onClick, + ) + } else { + this + } diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/SizeHelpers.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/SizeHelpers.kt new file mode 100644 index 0000000..a72c246 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/extensions/SizeHelpers.kt @@ -0,0 +1,11 @@ +package cl.homelogic.platform.designsystem.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Composable +fun Dp.scaledByFontSize(): Dp { + val fontScale = LocalDensity.current.fontScale + return this * fontScale +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Haptic.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Haptic.kt new file mode 100644 index 0000000..52a1cfa --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Haptic.kt @@ -0,0 +1,77 @@ +package cl.homelogic.platform.designsystem.foundations + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +enum class FeedbackType { + Confirm, + ContextClick, + GestureEnd, + GestureThresholdActivate, + LongPress, + Reject, + SegmentFrequentTick, + SegmentTick, + TextHandleMove, + ToggleOff, + ToggleOn, + VirtualKey, +} + +class HapticManager( + private val hapticFeedback: HapticFeedback, +) { + private val _hapticEnabled = MutableStateFlow(true) + val status: StateFlow = _hapticEnabled + + fun enable() { + _hapticEnabled.value = true + } + + fun disable() { + _hapticEnabled.value = false + } + + fun performFeedback( + type: FeedbackType, + ) { + if (!_hapticEnabled.value) return + + with(hapticFeedback) { + when (type) { + FeedbackType.Confirm -> performHapticFeedback(HapticFeedbackType.Confirm) + FeedbackType.ContextClick -> performHapticFeedback(HapticFeedbackType.ContextClick) + FeedbackType.GestureEnd -> performHapticFeedback(HapticFeedbackType.GestureEnd) + FeedbackType.GestureThresholdActivate -> performHapticFeedback(HapticFeedbackType.GestureThresholdActivate) + FeedbackType.LongPress -> performHapticFeedback(HapticFeedbackType.LongPress) + FeedbackType.Reject -> performHapticFeedback(HapticFeedbackType.Reject) + FeedbackType.SegmentFrequentTick -> performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) + FeedbackType.SegmentTick -> performHapticFeedback(HapticFeedbackType.SegmentTick) + FeedbackType.TextHandleMove -> performHapticFeedback(HapticFeedbackType.TextHandleMove) + FeedbackType.ToggleOff -> performHapticFeedback(HapticFeedbackType.ToggleOff) + FeedbackType.ToggleOn -> performHapticFeedback(HapticFeedbackType.ToggleOn) + FeedbackType.VirtualKey -> performHapticFeedback(HapticFeedbackType.VirtualKey) + } + } + } +} + +val LocalHapticManager = staticCompositionLocalOf { + error("HapticManager not provided") +} + +@Composable +fun HapticSystem( + hapticFeedback: HapticFeedback, + content: @Composable () -> Unit, +) { + val hapticManager = HapticManager(hapticFeedback) + CompositionLocalProvider(LocalHapticManager provides hapticManager) { + content() + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Palette.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Palette.kt new file mode 100644 index 0000000..5a1f37d --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Palette.kt @@ -0,0 +1,201 @@ +package cl.homelogic.platform.designsystem.foundations + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +sealed interface Palette { + val name: String + val color: Color + + sealed class Mono : Palette { + data object At0 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFFFFFFFF) + } + + data object At200 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFFF2F3F5) + } + + data object At250 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFFDBE0E5) + } + + data object At300 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFFC2C3C5) + } + + data object At400 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF9A9CA0) + } + + data object At500 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF78828D) + } + + data object At600 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF637587) + } + + data object At750 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF48525D) + } + + data object At800 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF3D4754) + } + + data object At850 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF38383D) + } + + data object At900 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF293038) + } + + data object At950 : Mono() { + override val name: String = this.toString() + override val color: Color = Color(0xFF121417) + } + } + + sealed class Brand : Palette { + data object Phoebe : Brand() { + override val name: String = this.toString() + override val color: Color = Color(0xFF10d48e) + } + + data object Joey : Brand() { + override val name: String = this.toString() + override val color: Color = Color(0xFF0fc081) + } + + data object Monica : Brand() { + override val name: String = this.toString() + override val color: Color = Color(0xFF132a3a) + } + + data object Chandler : Brand() { + override val name: String = this.toString() + override val color: Color = Color(0xFF1c3c52) + } + } + + sealed class Sky : Palette { + data object Clear : Sky() { + override val name: String = this.toString() + override val color: Color = Color(0xFF197fe6) + } + + data object Deep : Sky() { + override val name: String = this.toString() + override val color: Color = Color(0xFF005CAC) + } + } + + sealed class Hazard : Palette { + data object Fire : Hazard() { + override val name: String = this.toString() + override val color: Color = Color(0xFFC34836) + } + } + + sealed class Sunset : Palette { + data object Dawn : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFFF49E4E) + } + + data object Sunny : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFFFFA255) + } + + data object Dusk : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFFF5824E) + } + + data object Evening : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFFE07648) + } + + data object Twilight : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFF4E3151) + } + + data object Night : Sunset() { + override val name: String = this.toString() + override val color: Color = Color(0xFF191538) + } + } + + companion object { + fun getMonoColors(): List = + listOf( + Mono.At0, + Mono.At200, + Mono.At250, + Mono.At300, + Mono.At400, + Mono.At500, + Mono.At600, + Mono.At750, + Mono.At800, + Mono.At850, + Mono.At900, + Mono.At950, + ) + + fun getBrandColors(): List = + listOf( + Brand.Phoebe, + Brand.Joey, + Brand.Monica, + Brand.Chandler, + ) + + fun getSkyColors(): List = + listOf( + Sky.Clear, + Sky.Deep, + ) + + fun getHazardColors(): List = + listOf( + Hazard.Fire, + ) + + fun getSunsetColors(): List = + listOf( + Sunset.Dawn, + Sunset.Sunny, + Sunset.Dusk, + Sunset.Evening, + Sunset.Twilight, + Sunset.Night, + ) + + fun getAllColors(): List = + listOf( + getMonoColors(), + getBrandColors(), + getSkyColors(), + getHazardColors(), + getSunsetColors(), + ).flatten() + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Shapes.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Shapes.kt new file mode 100644 index 0000000..a4a4d28 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Shapes.kt @@ -0,0 +1,11 @@ +package cl.homelogic.platform.designsystem.foundations + +import androidx.compose.foundation.shape.RoundedCornerShape + +object Shapes { + object RoundCorner { + val small = RoundedCornerShape(Dimens.at6) + val regular = RoundedCornerShape(Dimens.at8) + val big = RoundedCornerShape(Dimens.at12) + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Sizes.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Sizes.kt new file mode 100644 index 0000000..ea58ae1 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Sizes.kt @@ -0,0 +1,130 @@ +package cl.homelogic.platform.designsystem.foundations + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +sealed class Sizes { + object Buttons { + val Small = Dimens.at40 + val Regular = Dimens.at48 + val Big = Dimens.at56 + val Large = Dimens.at72 + } + + object Dots { + val Small = Dimens.at6 + val Regular = Dimens.at8 + val Big = Dimens.at10 + } + + object TextFields { + val Regular = Dimens.at52 + } + + object Images { + val Regular = Dimens.at300 + val Big = Dimens.at350 + } + + object Borders { + val Regular = Dimens.at1 + } + + object Header { + val Regular = Dimens.at72 + } + + object Spacers { + val Regular = Dimens.at8 + val Big = Dimens.at16 + } + + object ScreenInset { + val Bottom = Dimens.at40 + } + + object Shadows { + val Regular = Dimens.at4 + } + + object Stroke { + val Regular = Dimens.at1 + } + + object Containers { + val Small = Dimens.at40 + val Regular = Dimens.at60 + val Big = Dimens.at72 + val Large = Dimens.at120 + val Huge = Dimens.at300 + } +} + +@Immutable +object Padding { + val none = Dimens.at0 + val tiny = Dimens.at4 + val small = Dimens.at8 + val regular = Dimens.at12 + val big = Dimens.at16 + val large = Dimens.at32 + + object Content { + val regular = PaddingValues(vertical = Dimens.at16) + } +} + +@Immutable +object Dimens { + val at0 = 0.dp + val at1 = 1.dp + val at2 = 2.dp + val at4 = 4.dp + val at6 = 6.dp + val at8 = 8.dp + val at10 = 10.dp + val at12 = 12.dp + val at16 = 16.dp + val at20 = 20.dp + val at24 = 24.dp + val at28 = 28.dp + val at32 = 32.dp + val at36 = 36.dp + val at40 = 40.dp + val at44 = 44.dp + val at48 = 48.dp + val at52 = 52.dp + val at56 = 56.dp + val at60 = 60.dp + val at72 = 72.dp + val at120 = 120.dp + val at300 = 300.dp + val at350 = 350.dp +} + +@Immutable +object ScaledDimens { + val at0 = 0.sp + val at2 = 2.sp + val at4 = 4.sp + val at8 = 8.sp + val at12 = 12.sp + val at16 = 16.sp + val at18 = 18.sp + val at20 = 20.sp + val at22 = 22.sp + val at24 = 24.sp + val at26 = 26.sp + val at28 = 28.sp + val at30 = 30.sp + val at32 = 32.sp + val at36 = 36.sp + val at40 = 40.sp + val at44 = 44.sp + val at48 = 48.sp + val at52 = 52.sp + val at56 = 56.sp + val at72 = 72.sp +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Symbols.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Symbols.kt new file mode 100644 index 0000000..57c0646 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Symbols.kt @@ -0,0 +1,60 @@ +package cl.homelogic.platform.designsystem.foundations + +import cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols.getPlatformIcon + +fun Symbols.unicode(): String = + this.codePoint[0] + .code + .toString(16) + .padStart(4, '0') + +// Get codepoints from: https://fonts.google.com/icons +sealed class Symbols( + val codePoint: String, +) { + data object IconSettings : Symbols("\ue8b8") + + data object IconArrowBack : Symbols(getPlatformIcon("\ue5c4", "\ue5e0")) + + data object IconClose : Symbols("\ue5cd") + + data object IconTextField : Symbols("\ue9f1") + + data object IconButtons : Symbols("\ue72f") + + data object IconHeader : Symbols("\uf384") + + data object IconSwitchCard : Symbols("\ue1f4") + + data object IconFonts : Symbols("\ue262") + + data object IconShortcut : Symbols("\ue7e1") + + data object IconSymbols : Symbols("\uf7f7") + + data object IconHeadline : Symbols("\ue23c") + + data object IconHeadlines : Symbols("\ue91a") + + data object IconTypography : Symbols("\ueb94") + + data object IconTwoColumns : Symbols("\uf847") + + data object IconGlyph : Symbols("\ue574") + + data object IconColors : Symbols("\ue40a") + + data object IconThemes : Symbols("\ue997") + + data object IconShapes : Symbols("\ue602") + + data object IconDimensions : Symbols("\ue41c") + + data object IconIllustrations : Symbols("\ue3f4") + + data object IconDarkMode : Symbols("\ue51c") + + data object IconSearch : Symbols("\ue8b6") + + data object IconPlayCircle : Symbols("\ue1c4") +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Themes.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Themes.kt new file mode 100644 index 0000000..381309b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Themes.kt @@ -0,0 +1,51 @@ +package cl.homelogic.platform.designsystem.foundations + +import cl.homelogic.platform.designsystem.themes.BaseTheme +import cl.homelogic.platform.designsystem.themes.ColorPair + +enum class Themes( + val definition: BaseTheme, +) { + ClassicVibes(Classic), + MintVibes(Mint), +} + +private object Classic : BaseTheme() { + override val primary = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val primaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val onPrimary = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val onPrimaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val secondary = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val secondaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900) + override val tertiary = ColorPair(Palette.Sky.Clear, Palette.Sky.Deep) + override val tertiaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900) + override val onTertiary = ColorPair(Palette.Mono.At0, Palette.Mono.At0) + override val surfaceContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val error = ColorPair(Palette.Hazard.Fire, Palette.Hazard.Fire) + override val surface = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val surfaceVariant = ColorPair(Palette.Mono.At200, Palette.Mono.At900) + override val onSurface = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val onSurfaceVariant = ColorPair(Palette.Mono.At600, Palette.Mono.At0) + override val background = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val outline = ColorPair(Palette.Mono.At250, Palette.Mono.At850) +} + +private object Mint : BaseTheme() { + override val primary = ColorPair(Palette.Brand.Monica, Palette.Brand.Joey) + override val primaryContainer = ColorPair(Palette.Mono.At0, Palette.Brand.Joey) + override val onPrimary = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val onPrimaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val secondary = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val secondaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900) + override val tertiary = ColorPair(Palette.Brand.Phoebe, Palette.Brand.Joey) + override val tertiaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900) + override val onTertiary = ColorPair(Palette.Mono.At0, Palette.Mono.At0) + override val surfaceContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val error = ColorPair(Palette.Hazard.Fire, Palette.Hazard.Fire) + override val surface = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val surfaceVariant = ColorPair(Palette.Mono.At200, Palette.Mono.At850) + override val onSurface = ColorPair(Palette.Mono.At950, Palette.Mono.At0) + override val onSurfaceVariant = ColorPair(Palette.Mono.At600, Palette.Mono.At0) + override val background = ColorPair(Palette.Mono.At0, Palette.Mono.At950) + override val outline = ColorPair(Palette.Mono.At250, Palette.Mono.At850) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Typography.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Typography.kt new file mode 100644 index 0000000..a6af80c --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/Typography.kt @@ -0,0 +1,77 @@ +package cl.homelogic.platform.designsystem.foundations + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontWeight +import cl.homelogic.platform.designsystem.foundations.fonts.manrope.ManropeFontFamily +import cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols.SymbolsOutlinedFontFamily + +@Composable +fun ManropeTypography() = + Typography().run { + val fontFamily = ManropeFontFamily() + copy( + displayLarge = displayLarge.copy( + fontFamily = SymbolsOutlinedFontFamily(), + ), + displayMedium = displayMedium.copy( + fontSize = ScaledDimens.at32, + fontFamily = SymbolsOutlinedFontFamily(), + fontWeight = FontWeight.Light, + ), + displaySmall = displaySmall.copy( + fontSize = ScaledDimens.at24, + fontFamily = SymbolsOutlinedFontFamily(), + fontWeight = FontWeight.Light, + lineHeight = ScaledDimens.at30, + ), + headlineLarge = headlineLarge.copy( + fontFamily = fontFamily, + fontSize = ScaledDimens.at26, + fontWeight = FontWeight.ExtraBold, + lineHeight = ScaledDimens.at48, + ), + headlineMedium = headlineMedium.copy( + fontFamily = fontFamily, + fontSize = ScaledDimens.at22, + fontWeight = FontWeight.Bold, + lineHeight = ScaledDimens.at28, + ), + headlineSmall = headlineSmall.copy( + fontFamily = fontFamily, + ), + titleLarge = titleLarge.copy( + fontFamily = fontFamily, + ), + titleMedium = titleMedium.copy( + fontFamily = fontFamily, + fontSize = ScaledDimens.at18, + fontWeight = FontWeight.ExtraBold, + lineHeight = ScaledDimens.at22, + ), + titleSmall = titleSmall.copy( + fontFamily = fontFamily, + ), + bodyLarge = bodyMedium.copy( + fontFamily = fontFamily, + fontWeight = FontWeight.ExtraBold, + ), + bodyMedium = bodyMedium.copy( + fontFamily = fontFamily, + fontWeight = FontWeight.Medium, + ), + bodySmall = bodySmall.copy( + fontFamily = fontFamily, + ), + labelLarge = labelLarge.copy( + fontFamily = fontFamily, + fontWeight = FontWeight.ExtraBold, + ), + labelMedium = labelMedium.copy( + fontFamily = fontFamily, + ), + labelSmall = labelSmall.copy( + fontFamily = fontFamily, + ), + ) + } diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/manrope/ManropeFontFamily.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/manrope/ManropeFontFamily.kt new file mode 100644 index 0000000..eddba56 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/manrope/ManropeFontFamily.kt @@ -0,0 +1,26 @@ +package cl.homelogic.platform.designsystem.foundations.fonts.manrope + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import cl.homelogic.platform.designsystem.resources.Manrope_Bold +import cl.homelogic.platform.designsystem.resources.Manrope_ExtraBold +import cl.homelogic.platform.designsystem.resources.Manrope_ExtraLight +import cl.homelogic.platform.designsystem.resources.Manrope_Light +import cl.homelogic.platform.designsystem.resources.Manrope_Medium +import cl.homelogic.platform.designsystem.resources.Manrope_Regular +import cl.homelogic.platform.designsystem.resources.Manrope_SemiBold +import cl.homelogic.platform.designsystem.resources.Res +import org.jetbrains.compose.resources.Font + +@Composable +fun ManropeFontFamily() = + FontFamily( + Font(Res.font.Manrope_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.Manrope_Light, weight = FontWeight.Light), + Font(Res.font.Manrope_Regular, weight = FontWeight.Normal), + Font(Res.font.Manrope_Medium, weight = FontWeight.Medium), + Font(Res.font.Manrope_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.Manrope_Bold, weight = FontWeight.Bold), + Font(Res.font.Manrope_ExtraBold, weight = FontWeight.ExtraBold), + ) diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/materialsymbols/MaterialSymbolsFontFamily.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/materialsymbols/MaterialSymbolsFontFamily.kt new file mode 100644 index 0000000..16646ff --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/foundations/fonts/materialsymbols/MaterialSymbolsFontFamily.kt @@ -0,0 +1,119 @@ +package cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import cl.homelogic.platform.common.Platform +import cl.homelogic.platform.common.PlatformTypes +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_SemiBold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_SemiBold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_SemiBold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_SemiBold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Bold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_ExtraLight +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_SemiBold +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Light +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Medium +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Regular +import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_SemiBold +import cl.homelogic.platform.designsystem.resources.Res +import org.jetbrains.compose.resources.Font + +@Composable +internal fun SymbolsSharpFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsSharp_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsSharp_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsSharp_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsSharp_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsSharp_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsSharp_Bold, weight = FontWeight.Bold), + ) + +@Composable +internal fun SymbolsSharpFilledFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsSharp_Filled_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsSharp_Filled_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsSharp_Filled_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsSharp_Filled_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsSharp_Filled_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsSharp_Filled_Bold, weight = FontWeight.Bold), + ) + +@Composable +internal fun SymbolsRoundedFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsRounded_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsRounded_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsRounded_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsRounded_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsRounded_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsRounded_Bold, weight = FontWeight.Bold), + ) + +@Composable +internal fun SymbolsRoundedFilledFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsRounded_Filled_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsRounded_Filled_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsRounded_Filled_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsRounded_Filled_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsRounded_Filled_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsRounded_Filled_Bold, weight = FontWeight.Bold), + ) + +@Composable +internal fun SymbolsOutlinedFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsOutlined_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsOutlined_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsOutlined_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsOutlined_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsOutlined_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsOutlined_Bold, weight = FontWeight.Bold), + ) + +@Composable +internal fun SymbolsOutlinedFilledFontFamily() = + FontFamily( + Font(Res.font.MaterialSymbolsOutlined_Filled_ExtraLight, weight = FontWeight.ExtraLight), + Font(Res.font.MaterialSymbolsOutlined_Filled_Light, weight = FontWeight.Light), + Font(Res.font.MaterialSymbolsOutlined_Filled_Regular, weight = FontWeight.Normal), + Font(Res.font.MaterialSymbolsOutlined_Filled_Medium, weight = FontWeight.Medium), + Font(Res.font.MaterialSymbolsOutlined_Filled_SemiBold, weight = FontWeight.SemiBold), + Font(Res.font.MaterialSymbolsOutlined_Filled_Bold, weight = FontWeight.Bold), + ) + +internal fun getPlatformIcon( + android: String, + ios: String, +) = when (Platform.getType()) { + PlatformTypes.IOS -> ios + PlatformTypes.Android -> android +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/BaseTheme.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/BaseTheme.kt new file mode 100644 index 0000000..14557c8 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/BaseTheme.kt @@ -0,0 +1,73 @@ +package cl.homelogic.platform.designsystem.themes + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import cl.homelogic.platform.designsystem.foundations.Palette + +data class ColorPair( + val light: Palette, + val dark: Palette, +) + +abstract class BaseTheme { + abstract val primary: ColorPair + abstract val primaryContainer: ColorPair + abstract val onPrimary: ColorPair + abstract val onPrimaryContainer: ColorPair + abstract val secondary: ColorPair + abstract val secondaryContainer: ColorPair + abstract val tertiary: ColorPair + abstract val tertiaryContainer: ColorPair + abstract val onTertiary: ColorPair + abstract val surfaceContainer: ColorPair + abstract val error: ColorPair + abstract val surface: ColorPair + abstract val surfaceVariant: ColorPair + abstract val onSurface: ColorPair + abstract val onSurfaceVariant: ColorPair + abstract val background: ColorPair + abstract val outline: ColorPair + + val lightVariant: ColorScheme + get() = lightColorScheme( + primary = primary.light.color, + primaryContainer = primaryContainer.light.color, + onPrimary = onPrimary.light.color, + onPrimaryContainer = onPrimaryContainer.light.color, + secondary = secondary.light.color, + secondaryContainer = secondaryContainer.light.color, + tertiary = tertiary.light.color, + tertiaryContainer = tertiaryContainer.light.color, + onTertiary = onTertiary.light.color, + surfaceContainer = surfaceContainer.light.color, + error = error.light.color, + surface = surface.light.color, + surfaceVariant = surfaceVariant.light.color, + onSurface = onSurface.light.color, + onSurfaceVariant = onSurfaceVariant.light.color, + background = background.light.color, + outline = outline.light.color, + ) + + val darkVariant: ColorScheme + get() = darkColorScheme( + primary = primary.dark.color, + primaryContainer = primaryContainer.dark.color, + onPrimary = onPrimary.dark.color, + onPrimaryContainer = onPrimaryContainer.dark.color, + secondary = secondary.dark.color, + secondaryContainer = secondaryContainer.dark.color, + tertiary = tertiary.dark.color, + tertiaryContainer = tertiaryContainer.dark.color, + onTertiary = onTertiary.dark.color, + surfaceContainer = surfaceContainer.dark.color, + error = error.dark.color, + surface = surface.dark.color, + surfaceVariant = surfaceVariant.dark.color, + onSurface = onSurface.dark.color, + onSurfaceVariant = onSurfaceVariant.dark.color, + background = background.dark.color, + outline = outline.dark.color, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystem.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystem.kt new file mode 100644 index 0000000..b7b5d61 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystem.kt @@ -0,0 +1,41 @@ +package cl.homelogic.platform.designsystem.themes + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalHapticFeedback +import cl.homelogic.platform.designsystem.foundations.HapticSystem +import cl.homelogic.platform.designsystem.foundations.ManropeTypography + +val LocalDesignSystem = staticCompositionLocalOf { + error("DesignSystemManager not provided") +} + +@Composable +fun DesignSystem( + designSystemManager: DesignSystemManager, + content: @Composable () -> Unit, +) { + val state = designSystemManager.state.collectAsState().value + + HapticSystem( + hapticFeedback = LocalHapticFeedback.current, + ) { + MaterialTheme( + colorScheme = when (state.darkMode) { + DarkModeState.Disabled -> state.theme.definition.lightVariant + DarkModeState.Enabled -> state.theme.definition.darkVariant + DarkModeState.Oled -> state.theme.definition.darkVariant + }, + typography = ManropeTypography(), + ) { + CompositionLocalProvider( + LocalDesignSystem provides designSystemManager, + ) { + content() + } + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystemManager.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystemManager.kt new file mode 100644 index 0000000..9f3b75a --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/DesignSystemManager.kt @@ -0,0 +1,63 @@ +package cl.homelogic.platform.designsystem.themes + +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots +import cl.homelogic.platform.designsystem.foundations.Themes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +data class DesignSystemState( + val theme: Themes, + val darkMode: DarkModeState, +) + +enum class DarkModeState { + Disabled, + Enabled, + Oled, +} + +class DesignSystemManager( + theme: Themes, + darkMode: DarkModeState, +) { + private val _coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val _statusBarManager = StatusBarManager() + + private val _state = + MutableStateFlow(DesignSystemState(theme, darkMode)) + val state: StateFlow = _state + + init { + Trace.d(TreeRoots.DesignSystem, "Initializing Design System") + } + + fun setTheme(theme: Themes) { + Trace.i(TreeRoots.DesignSystem, "Changing theme state to: ${theme.name}") + _coroutineScope.launch { + _state.emit(_state.value.copy(theme = theme)) + } + } + + fun getTheme(): Themes = _state.value.theme + + fun getDarkMode() = _state.value.darkMode + + fun setDarkMode(darkMode: DarkModeState) { + Trace.i(TreeRoots.DesignSystem, "Changing dark mode state to: ${darkMode.name}") + when (darkMode) { + DarkModeState.Disabled -> _statusBarManager.lightMode() + DarkModeState.Enabled -> _statusBarManager.darkMode() + DarkModeState.Oled -> _statusBarManager.darkMode() + } + + _coroutineScope.launch { + _state.emit(_state.value.copy(darkMode = darkMode)) + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.kt b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.kt new file mode 100644 index 0000000..3c4db41 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.kt @@ -0,0 +1,10 @@ +package cl.homelogic.platform.designsystem.themes + +interface IStatusBarManager { + fun lightMode() + + fun darkMode() +} + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +expect class StatusBarManager() : IStatusBarManager diff --git a/core/designsystem/src/iosMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.ios.kt b/core/designsystem/src/iosMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.ios.kt new file mode 100644 index 0000000..3eb1d77 --- /dev/null +++ b/core/designsystem/src/iosMain/kotlin/cl/homelogic/platform/designsystem/themes/StatusBarManager.ios.kt @@ -0,0 +1,27 @@ +package cl.homelogic.platform.designsystem.themes + +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots +import platform.UIKit.UIApplication +import platform.UIKit.UIStatusBarStyleDarkContent +import platform.UIKit.UIStatusBarStyleLightContent +import platform.UIKit.setStatusBarStyle + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class StatusBarManager : IStatusBarManager { + override fun lightMode() { + Trace.d(TreeRoots.DesignSystem, "iOS - requesting light status bar") + UIApplication.sharedApplication.setStatusBarStyle( + UIStatusBarStyleLightContent, + animated = true, + ) + } + + override fun darkMode() { + Trace.d(TreeRoots.DesignSystem, "iOS - requesting dark status bar") + UIApplication.sharedApplication.setStatusBarStyle( + UIStatusBarStyleDarkContent, + animated = true, + ) + } +} diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 0000000..050927c --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,26 @@ +group = "dev.carlosmartino.navigation" + +plugins { + alias(libs.plugins.kotlinDevKit) + alias(libs.plugins.composeDevKit) +} + +kotlin { + sourceSets { + commonMain.dependencies { + api(libs.compose.multiplatform.navigation) + implementation(projects.core.common) + implementation(projects.core.designsystem) + } + + androidMain.dependencies { + api(libs.androidx.compose.navigation) + } + } +} + +compose.resources { + publicResClass = true + packageOfResClass = "dev.carlosmartino.navigation.resources" + generateResClass = always +} diff --git a/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationExtensions.kt b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationExtensions.kt new file mode 100644 index 0000000..ec5dd75 --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationExtensions.kt @@ -0,0 +1,60 @@ +package cl.homelogic.platform.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots + +inline fun NavGraphBuilder.createRoute( + transition: NavigationTransition = NavigationTransition.FLOW, + deepLinks: List = listOf(), + crossinline content: @Composable (T) -> Unit, +) { + composable( +// enterTransition = { +// when (transition) { +// NavigationTransition.FLOW -> slideInHorizontally() +// NavigationTransition.SINGLE -> NavigationAnimations.verticalEnter +// } +// }, +// exitTransition = { +// when (transition) { +// NavigationTransition.FLOW -> slideOutHorizontally() +// NavigationTransition.SINGLE -> NavigationAnimations.verticalExit +// } +// }, +// popEnterTransition = { +// when (transition) { +// NavigationTransition.FLOW -> NavigationAnimations.horizontalPopEnter +// NavigationTransition.SINGLE -> NavigationAnimations.verticalPopEnter +// } +// }, +// popExitTransition = { +// when (transition) { +// NavigationTransition.FLOW -> slideOutHorizontally() +// NavigationTransition.SINGLE -> NavigationAnimations.verticalPopExit +// } +// }, + deepLinks = deepLinks, + ) { backStackEntry -> + val route: T = remember { backStackEntry.toRoute() } + val isFirstComposition = remember { true } + if (isFirstComposition) { + Trace.i( + TreeRoots.Navigation, + "Route created: ${ + route::class + .qualifiedName + ?.split(".") + ?.takeLast(2) + ?.joinToString(".") ?: route::class.simpleName + }", + ) + content(route) + } + } +} diff --git a/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationManager.kt b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationManager.kt new file mode 100644 index 0000000..330a623 --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationManager.kt @@ -0,0 +1,101 @@ +package cl.homelogic.platform.navigation + +import androidx.navigation.NavUri +import cl.homelogic.platform.common.logging.Trace +import cl.homelogic.platform.common.logging.TreeRoots +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlin.time.Clock +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant + +sealed class NavigationCommand { + data class Route( + val destination: NavigationDestination, + ) : NavigationCommand() + + data class DeepLink( + val uri: NavUri, + ) : NavigationCommand() + + data object PopBack : NavigationCommand() + + data class PopUpTo( + val destination: NavigationDestination, + val inclusive: Boolean = false, + ) : NavigationCommand() +} + +class NavigationManager { + private val _navigationEvents = MutableSharedFlow() + internal val navigationEvents = _navigationEvents.asSharedFlow() + + private val throttleWindow = 100.milliseconds + private var lastNavigationTime: Instant = Instant.DISTANT_PAST + + suspend fun navigate(destination: NavigationDestination) { + if (shouldThrottleNavigation()) { + Trace.d(TreeRoots.Navigation, "Navigation throttled: ${destination::class}") + return + } + + Trace.d( + TreeRoots.Navigation, + "Navigating to: ${ + destination::class + .qualifiedName + ?.split(".") + ?.takeLast(2) + ?.joinToString(".") ?: destination::class.simpleName + }", + ) + updateLastNavigationTime() + _navigationEvents.emit(NavigationCommand.Route(destination)) + } + + suspend fun navigate(deepLink: NavUri) { + if (shouldThrottleNavigation()) { + Trace.d(TreeRoots.Navigation, "DeepLink navigation throttled for: $deepLink") + return + } + + Trace.d(TreeRoots.Navigation, "Navigating to deeplink: $deepLink") + updateLastNavigationTime() + _navigationEvents.emit(NavigationCommand.DeepLink(deepLink)) + } + + suspend fun popBackStack() { + if (shouldThrottleNavigation()) { + Trace.d(TreeRoots.Navigation, "PopBackStack throttled") + return + } + + Trace.d(TreeRoots.Navigation, "PopBackStack triggered") + updateLastNavigationTime() + _navigationEvents.emit(NavigationCommand.PopBack) + } + + suspend fun popUpTo( + destination: NavigationDestination, + inclusive: Boolean, + ) { + if (shouldThrottleNavigation()) { + Trace.d(TreeRoots.Navigation, "PopBackStack throttled") + return + } + + Trace.d(TreeRoots.Navigation, "PopBackStack triggered") + updateLastNavigationTime() + _navigationEvents.emit(NavigationCommand.PopUpTo(destination, inclusive)) + } + + private fun shouldThrottleNavigation(): Boolean { + val currentTime = Clock.System.now() + val lastNavigation = lastNavigationTime + return (currentTime - lastNavigation) < throttleWindow + } + + private fun updateLastNavigationTime() { + lastNavigationTime = Clock.System.now() + } +} diff --git a/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationStructure.kt b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationStructure.kt new file mode 100644 index 0000000..fe12d5e --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationStructure.kt @@ -0,0 +1,13 @@ +package cl.homelogic.platform.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController + +interface NavigationModule { + fun navigationGraph( + navGraphBuilder: NavGraphBuilder, + navController: NavHostController, + ) +} + +interface NavigationDestination diff --git a/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationSystem.kt b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationSystem.kt new file mode 100644 index 0000000..caf30e8 --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationSystem.kt @@ -0,0 +1,65 @@ +package cl.homelogic.platform.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.navigation.NavUri +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.moriatsushi.insetsx.statusBars +import kotlinx.coroutines.flow.MutableSharedFlow + +@Composable +fun NavigationSystem( + navigationManager: NavigationManager, + deeplinkStream: MutableSharedFlow? = null, + startDestination: NavigationDestination, + navigationModules: List, +) { + val navController = rememberNavController() + + LaunchedEffect(navController) { + deeplinkStream?.collect { + navController.navigate(NavUri(it)) + } + } + + LaunchedEffect(navController) { + navigationManager.navigationEvents.collect { command -> + when (command) { + is NavigationCommand.Route -> navController.navigate(command.destination) + is NavigationCommand.DeepLink -> navController.navigate(command.uri) + is NavigationCommand.PopBack -> navController.popBackStack() + is NavigationCommand.PopUpTo -> navController.popBackStack( + command.destination, + command.inclusive, + ) + } + } + } + + Box( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + .padding( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + ), + ) { + NavHost( + navController = navController, + startDestination = startDestination, + ) { + navigationModules.forEach { module -> + module.navigationGraph(this, navController) + } + } + } +} diff --git a/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationTransition.kt b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationTransition.kt new file mode 100644 index 0000000..53cc0db --- /dev/null +++ b/core/navigation/src/commonMain/kotlin/cl/homelogic/platform/navigation/NavigationTransition.kt @@ -0,0 +1,79 @@ +package cl.homelogic.platform.navigation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically + +enum class NavigationTransition { + FLOW, + SINGLE, +} + +object NavigationAnimations { + private const val ANIMATION_DURATION = 200 + private const val OVERLAY_SCALE_REDUCED = 0.95f + private const val OVERLAY_OPACITY = 0.3f + + val horizontalEnter: EnterTransition = slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(ANIMATION_DURATION), + ) + fadeIn( + animationSpec = tween(ANIMATION_DURATION), + ) + + val horizontalExit: ExitTransition = fadeOut( + animationSpec = tween(ANIMATION_DURATION), + ) + scaleOut( + targetScale = OVERLAY_SCALE_REDUCED, + animationSpec = tween(ANIMATION_DURATION), + ) + + val horizontalPopEnter: EnterTransition = fadeIn( + animationSpec = tween(ANIMATION_DURATION), + ) + scaleIn( + initialScale = OVERLAY_SCALE_REDUCED, + animationSpec = tween(ANIMATION_DURATION), + ) + + val horizontalPopExit: ExitTransition = slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(ANIMATION_DURATION), + ) + fadeOut( + animationSpec = tween(ANIMATION_DURATION), + ) + + // For vertical modal transitions + val verticalEnter: EnterTransition = slideInVertically( + initialOffsetY = { fullHeight -> fullHeight }, + animationSpec = tween(ANIMATION_DURATION), + ) + + val verticalExit: ExitTransition = fadeOut( + targetAlpha = OVERLAY_OPACITY, + animationSpec = tween(ANIMATION_DURATION), + ) + scaleOut( + targetScale = OVERLAY_SCALE_REDUCED, + animationSpec = tween(ANIMATION_DURATION), + ) + + val verticalPopEnter: EnterTransition = fadeIn( + initialAlpha = OVERLAY_OPACITY, + animationSpec = tween(ANIMATION_DURATION), + ) + scaleIn( + initialScale = OVERLAY_SCALE_REDUCED, + animationSpec = tween(ANIMATION_DURATION), + ) + + val verticalPopExit: ExitTransition = slideOutVertically( + targetOffsetY = { fullHeight -> fullHeight }, + animationSpec = tween(ANIMATION_DURATION), + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42056c6..733192c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,23 +3,79 @@ android-compileSdk = "36" android-minSdk = "24" android-targetSdk = "36" +# Platform specific versions agp = "8.13.0" +kotlin = "2.2.20" androidx-activity = "1.11.0" -androidx-appcompat = "1.7.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.4" androidx-testExt = "1.3.0" +androidx-navigation = "2.9.4" +androidx-security = "1.1.0" +androidx-datastore = "1.1.7" +kotlinx-serialization = "1.9.0" +kotlinx-datetime = "0.7.1" +kotlinx-coroutines = "1.10.2" +compose-multiplatform-navigation = "2.9.0" compose-multiplatform = "1.9.0" +# Additional Libraries +koin = "4.1.1" +insetsx = "0.1.0-alpha10" +atomicfu = "0.29.0" +markdown = "0.7.3" +multiplatform-settings = "1.3.0" +moko-permissions = "0.20.1" +# Testing junit = "4.13.2" -kotlin = "2.2.20" [libraries] +# Platform specific libraries androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } -androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidx-security" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } +androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-datastore" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime"} +compose-multiplatform-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose-multiplatform-navigation" } + +# Koin - Dependency Injection +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } + +# MOKO +moko-permissions = { module = "dev.icerock.moko:permissions", version.ref = "moko-permissions"} +moko-permissions-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "moko-permissions"} +moko-permissions-bluetooth = { module = "dev.icerock.moko:permissions-bluetooth", version.ref = "moko-permissions"} +moko-permissions-camera = { module = "dev.icerock.moko:permissions-camera", version.ref = "moko-permissions"} +moko-permissions-contacts = { module = "dev.icerock.moko:permissions-contacts", version.ref = "moko-permissions"} +moko-permissions-gallery = { module = "dev.icerock.moko:permissions-gallery", version.ref = "moko-permissions"} +moko-permissions-location = { module = "dev.icerock.moko:permissions-location", version.ref = "moko-permissions"} +moko-permissions-microphone = { module = "dev.icerock.moko:permissions-microphone", version.ref = "moko-permissions"} +moko-permissions-motion = { module = "dev.icerock.moko:permissions-motion", version.ref = "moko-permissions"} +moko-permissions-notifications = { module = "dev.icerock.moko:permissions-notifications", version.ref = "moko-permissions"} +moko-permissions-storage = { module = "dev.icerock.moko:permissions-storage", version.ref = "moko-permissions"} + +# Multiplatform-settings +multiplatform-settings-core = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } +multiplatform-settings-noarg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" } +multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" } +multiplatform-settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization", version.ref = "multiplatform-settings" } + +# Others +insetsx = { module = "com.moriatsushi.insetsx:insetsx", version.ref = "insetsx" } +atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } +markdown = { module = "org.jetbrains:markdown", version.ref = "markdown"} # Test libraries junit = { module = "junit:junit", version.ref = "junit" } @@ -40,4 +96,4 @@ kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", versio # Buildlogic defined plugins -> buildlogic/convention/build.gradle.kts composeDevKit = { id = "dev.carlosmartino.plugins.composeMultiplatform", version = "unspecified" } -kotlinDevKit = { id = "dev.carlosmartino.plugins.kotlinMultiplatform", version = "unspecified" } \ No newline at end of file +kotlinDevKit = { id = "dev.carlosmartino.plugins.kotlinMultiplatform", version = "unspecified" } diff --git a/settings.gradle.kts b/settings.gradle.kts index ea85b4e..0172bf8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,6 @@ dependencyResolutionManagement { } include(":app:composeApp") +include(":core:designsystem") +include(":core:common") +include(":core:navigation")