├── .gitignore ├── .idea ├── .gitignore ├── gradle.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── assets └── hero.jpeg ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── com │ │ │ ├── builtwithpaper │ │ │ │ └── ContainerSize.android.kt │ │ │ └── example │ │ │ │ └── MainActivity.kt │ │ └── material3 │ │ │ ├── Ripple.kt │ │ │ └── tokens │ │ │ └── StateTokens.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ ├── commonMain │ └── kotlin │ │ └── com │ │ ├── builtwithpaper │ │ ├── BottomNavigation.kt │ │ ├── Buttons.kt │ │ ├── Card.kt │ │ ├── CheckBox.kt │ │ ├── ContainerSize.kt │ │ ├── Modifier.kt │ │ ├── ProgressBar.kt │ │ ├── RadioButton.kt │ │ ├── ScreenWidthBreakpoint.kt │ │ ├── Slider.kt │ │ ├── SocialIcons.kt │ │ ├── Text.kt │ │ ├── TextField.kt │ │ ├── TextInput.kt │ │ ├── Theme.kt │ │ ├── Toggle.kt │ │ └── TopAppBar.kt │ │ └── example │ │ ├── AddContact.kt │ │ ├── App.kt │ │ ├── Contacts.kt │ │ ├── Conversation.kt │ │ ├── Conversations.kt │ │ ├── Profile.kt │ │ ├── Search.kt │ │ └── Settings.kt │ └── iosMain │ └── kotlin │ └── com │ ├── builtwithpaper │ ├── ContainerSize.ios.kt │ └── LightIndication.kt │ └── example │ └── MainViewController.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | **/build/ 4 | xcuserdata 5 | !src/**/build/ 6 | local.properties 7 | kotlin-js-store/ 8 | .DS_Store 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | *.xcodeproj/* 13 | !*.xcodeproj/project.pbxproj 14 | !*.xcodeproj/xcshareddata/ 15 | !*.xcodeproj/project.xcworkspace/ 16 | !*.xcworkspace/contents.xcworkspacedata 17 | **/xcshareddata/WorkspaceSettings.xcsettings 18 | venv/ 19 | .cache/ 20 | .kotlin/ 21 | 22 | # CMake 23 | cmake-build-*/ 24 | 25 | # File-based project format 26 | *.iws 27 | 28 | # IntelliJ 29 | out/ 30 | 31 | state_ui.json 32 | templates.json 33 | keyValueStore.txt -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Composable Horizons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bubbles 2 | 3 | This app was designed and exported entirely via Paper. 4 | 5 | No code was written by hand. You can [customize this app by clicking here](https://builtwithpaper.com/app/?app_template=bubbles). 6 | 7 | ![Preview](/assets/hero.jpeg) 8 | 9 | ## Project Structure 10 | 11 | There are 3 important folders in `composeApp/src`: 12 | 13 | - commonMain: Contains the shared sourceSet across all targets. This is where your designs live. 14 | - androidMain: Contains Android specific theming, such as the Material RippleEffect. 15 | - iosMain: iOS specific theming such as the light highlight. 16 | 17 | ## Supported Targets 18 | 19 | Paper currently exports to iOS and Android -------------------------------------------------------------------------------- /assets/hero.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/assets/hero.jpeg -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.composeMultiplatform) apply false 6 | alias(libs.plugins.composeCompiler) apply false 7 | alias(libs.plugins.kotlinMultiplatform) apply false 8 | } -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.kotlinMultiplatform) 6 | alias(libs.plugins.androidApplication) 7 | alias(libs.plugins.composeMultiplatform) 8 | alias(libs.plugins.composeCompiler) 9 | } 10 | 11 | kotlin { 12 | androidTarget { 13 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 14 | compilerOptions { 15 | jvmTarget.set(JvmTarget.JVM_11) 16 | } 17 | } 18 | 19 | listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> 20 | iosTarget.binaries.framework { 21 | baseName = "ComposeApp" 22 | isStatic = true 23 | } 24 | } 25 | 26 | sourceSets { 27 | commonMain.dependencies { 28 | implementation(compose.runtime) 29 | implementation(compose.foundation) 30 | implementation(compose.ui) 31 | implementation(compose.components.resources) 32 | implementation(compose.components.uiToolingPreview) 33 | implementation(libs.androidx.lifecycle.viewmodel) 34 | implementation(libs.androidx.lifecycle.runtime.compose) 35 | implementation(libs.navigation.compose) 36 | implementation(libs.compose.unstyled) 37 | 38 | implementation(libs.addcoilmultiplatform) 39 | implementation(libs.icons.lucide) 40 | } 41 | 42 | androidMain.dependencies { 43 | implementation(compose.preview) 44 | implementation(libs.androidx.activity.compose) 45 | implementation(libs.androidx.material.ripple) 46 | implementation("androidx.window:window:1.3.0") 47 | } 48 | } 49 | } 50 | 51 | android { 52 | namespace = "com.example" 53 | compileSdk = libs.versions.android.compileSdk.get().toInt() 54 | 55 | defaultConfig { 56 | applicationId = "com.example" 57 | minSdk = libs.versions.android.minSdk.get().toInt() 58 | targetSdk = libs.versions.android.targetSdk.get().toInt() 59 | versionCode = 1 60 | versionName = "1.0" 61 | } 62 | packaging { 63 | resources { 64 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 65 | } 66 | } 67 | buildTypes { 68 | getByName("release") { 69 | isMinifyEnabled = false 70 | } 71 | } 72 | compileOptions { 73 | sourceCompatibility = JavaVersion.VERSION_11 74 | targetCompatibility = JavaVersion.VERSION_11 75 | } 76 | } 77 | 78 | dependencies { 79 | debugImplementation(compose.uiTooling) 80 | } 81 | 82 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/builtwithpaper/ContainerSize.android.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.toComposeRect 5 | import androidx.compose.ui.platform.LocalConfiguration 6 | import androidx.compose.ui.platform.LocalContext 7 | import androidx.compose.ui.platform.LocalDensity 8 | import androidx.compose.ui.unit.DpSize 9 | import androidx.window.layout.WindowMetricsCalculator 10 | 11 | @Composable 12 | actual fun currentContainerSize(): DpSize { 13 | // Observe view configuration changes and recalculate the size class on each change. We can't 14 | // use Activity#onConfigurationChanged as this will sometimes fail to be called on different 15 | // API levels, hence why this function needs to be @Composable so we can observe the 16 | // ComposeView's configuration changes. 17 | LocalConfiguration.current 18 | val density = LocalDensity.current 19 | val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(LocalContext.current) 20 | return with(density) { metrics.bounds.toComposeRect().size.toDpSize() } 21 | } 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.LocalIndication 7 | import androidx.compose.material3.ripple 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | 10 | class MainActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | setContent { 15 | CompositionLocalProvider(LocalIndication provides ripple()) { 16 | App() 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/material3/Ripple.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package androidx.compose.material3 18 | 19 | import androidx.compose.foundation.Indication 20 | import androidx.compose.foundation.IndicationNodeFactory 21 | import androidx.compose.foundation.interaction.Interaction 22 | import androidx.compose.foundation.interaction.InteractionSource 23 | import androidx.compose.foundation.interaction.PressInteraction 24 | import androidx.compose.material.ripple.RippleAlpha 25 | import androidx.compose.material.ripple.createRippleModifierNode 26 | import androidx.compose.material3.tokens.StateTokens 27 | import androidx.compose.runtime.Immutable 28 | import androidx.compose.runtime.ProvidableCompositionLocal 29 | import androidx.compose.runtime.Stable 30 | import androidx.compose.runtime.compositionLocalOf 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.graphics.ColorProducer 33 | import androidx.compose.ui.graphics.isSpecified 34 | import androidx.compose.ui.node.CompositionLocalConsumerModifierNode 35 | import androidx.compose.ui.node.DelegatableNode 36 | import androidx.compose.ui.node.DelegatingNode 37 | import androidx.compose.ui.node.ObserverModifierNode 38 | import androidx.compose.ui.node.currentValueOf 39 | import androidx.compose.ui.node.observeReads 40 | import androidx.compose.ui.unit.Dp 41 | 42 | /** 43 | * Creates a Ripple using the provided values and values inferred from the theme. 44 | * 45 | * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s by 46 | * drawing ripple animations and state layers. 47 | * 48 | * A Ripple responds to [PressInteraction.Press] by starting a new ripple animation, and responds to 49 | * other [Interaction]s by showing a fixed state layer with varying alpha values depending on the 50 | * [Interaction]. 51 | * 52 | * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple 53 | * will be used as the default [Indication] inside components such as 54 | * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in addition 55 | * to Material provided components that use a Ripple as well. 56 | * 57 | * You can also explicitly create a Ripple and provide it to custom components in order to change 58 | * the parameters from the default, such as to create an unbounded ripple with a fixed size. 59 | * 60 | * To create a Ripple with a manually defined color that can change over time, see the other 61 | * [ripple] overload with a [ColorProducer] parameter. This will avoid unnecessary recompositions 62 | * when changing the color, and preserve existing ripple state when the color changes. 63 | * 64 | * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded ripples 65 | * always animate from the target layout center, bounded ripples animate from the touch position. 66 | * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be 67 | * calculated based on the target layout size. 68 | * @param color the color of the ripple. This color is usually the same color used by the text or 69 | * iconography in the component. This color will then have [RippleDefaults.RippleAlpha] applied to 70 | * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color 71 | * used will be [LocalContentColor] instead. 72 | */ 73 | @Stable 74 | fun ripple( 75 | bounded: Boolean = true, 76 | radius: Dp = Dp.Unspecified, 77 | color: Color = Color.Unspecified 78 | ): IndicationNodeFactory { 79 | return if (radius == Dp.Unspecified && color == Color.Unspecified) { 80 | if (bounded) return DefaultBoundedRipple else DefaultUnboundedRipple 81 | } else { 82 | RippleNodeFactory(bounded, radius, color) 83 | } 84 | } 85 | 86 | /** 87 | * Creates a Ripple using the provided values and values inferred from the theme. 88 | * 89 | * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s by 90 | * drawing ripple animations and state layers. 91 | * 92 | * A Ripple responds to [PressInteraction.Press] by starting a new ripple animation, and responds to 93 | * other [Interaction]s by showing a fixed state layer with varying alpha values depending on the 94 | * [Interaction]. 95 | * 96 | * [MaterialTheme] provides Ripples using [androidx.compose.foundation.LocalIndication], so a Ripple 97 | * will be used as the default [Indication] inside components such as 98 | * [androidx.compose.foundation.clickable] and [androidx.compose.foundation.indication], in addition 99 | * to Material provided components that use a Ripple as well. 100 | * 101 | * You can also explicitly create a Ripple and provide it to custom components in order to change 102 | * the parameters from the default, such as to create an unbounded ripple with a fixed size. 103 | * 104 | * To create a Ripple with a static color, see the [ripple] overload with a [Color] parameter. This 105 | * overload is optimized for Ripples that have dynamic colors that change over time, to reduce 106 | * unnecessary recompositions. 107 | * 108 | * @param color the color of the ripple. This color is usually the same color used by the text or 109 | * iconography in the component. This color will then have [RippleDefaults.RippleAlpha] applied to 110 | * calculate the final color used to draw the ripple. If you are creating this [ColorProducer] 111 | * outside of composition (where it will be automatically remembered), make sure that its instance 112 | * is stable (such as by remembering the object that holds it), or remember the returned [ripple] 113 | * object to make sure that ripple nodes are not being created each recomposition. 114 | * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded ripples 115 | * always animate from the target layout center, bounded ripples animate from the touch position. 116 | * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be 117 | * calculated based on the target layout size. 118 | */ 119 | @Stable 120 | fun ripple( 121 | color: ColorProducer, 122 | bounded: Boolean = true, 123 | radius: Dp = Dp.Unspecified 124 | ): IndicationNodeFactory { 125 | return RippleNodeFactory(bounded, radius, color) 126 | } 127 | 128 | /** Default values used by [ripple]. */ 129 | object RippleDefaults { 130 | /** 131 | * Represents the default [RippleAlpha] that will be used for a ripple to indicate different 132 | * states. 133 | */ 134 | val RippleAlpha: RippleAlpha = 135 | RippleAlpha( 136 | pressedAlpha = StateTokens.PressedStateLayerOpacity, 137 | focusedAlpha = StateTokens.FocusStateLayerOpacity, 138 | draggedAlpha = StateTokens.DraggedStateLayerOpacity, 139 | hoveredAlpha = StateTokens.HoverStateLayerOpacity 140 | ) 141 | } 142 | 143 | /** 144 | * CompositionLocal used for providing [RippleConfiguration] down the tree. This acts as a 145 | * tree-local 'override' for ripples used inside components that you cannot directly control, such 146 | * as to change the color of a specific component's ripple, or disable it entirely by providing 147 | * `null`. 148 | * 149 | * In most cases you should rely on the default theme behavior for consistency with other components 150 | * - this exists as an escape hatch for individual components and is not intended to be used for 151 | * full theme customization across an application. For this use case you should instead build your 152 | * own custom ripple that queries your design system theme values directly using 153 | * [createRippleModifierNode]. 154 | */ 155 | val LocalRippleConfiguration: ProvidableCompositionLocal = 156 | compositionLocalOf { 157 | RippleConfiguration() 158 | } 159 | 160 | /** 161 | * Configuration for [ripple] appearance, provided using [LocalRippleConfiguration]. In most cases 162 | * the default values should be used, for custom design system use cases you should instead build 163 | * your own custom ripple using [createRippleModifierNode]. To disable the ripple, provide `null` 164 | * using [LocalRippleConfiguration]. 165 | * 166 | * @param color the color override for the ripple. If [Color.Unspecified], then the default color 167 | * from the theme will be used instead. Note that if the ripple has a color explicitly set with 168 | * the parameter on [ripple], that will always be used instead of this value. 169 | * @param rippleAlpha the [RippleAlpha] override for this ripple. If null, then the default alpha 170 | * will be used instead. 171 | */ 172 | @Immutable 173 | class RippleConfiguration( 174 | val color: Color = Color.Unspecified, 175 | val rippleAlpha: RippleAlpha? = null 176 | ) { 177 | override fun equals(other: Any?): Boolean { 178 | if (this === other) return true 179 | if (other !is RippleConfiguration) return false 180 | 181 | if (color != other.color) return false 182 | if (rippleAlpha != other.rippleAlpha) return false 183 | 184 | return true 185 | } 186 | 187 | override fun hashCode(): Int { 188 | var result = color.hashCode() 189 | result = 31 * result + (rippleAlpha?.hashCode() ?: 0) 190 | return result 191 | } 192 | 193 | override fun toString(): String { 194 | return "RippleConfiguration(color=$color, rippleAlpha=$rippleAlpha)" 195 | } 196 | } 197 | 198 | @Stable 199 | private class RippleNodeFactory 200 | private constructor( 201 | private val bounded: Boolean, 202 | private val radius: Dp, 203 | private val colorProducer: ColorProducer?, 204 | private val color: Color 205 | ) : IndicationNodeFactory { 206 | constructor( 207 | bounded: Boolean, 208 | radius: Dp, 209 | colorProducer: ColorProducer 210 | ) : this(bounded, radius, colorProducer, Color.Unspecified) 211 | 212 | constructor(bounded: Boolean, radius: Dp, color: Color) : this(bounded, radius, null, color) 213 | 214 | override fun create(interactionSource: InteractionSource): DelegatableNode { 215 | val colorProducer = colorProducer ?: ColorProducer { color } 216 | return DelegatingThemeAwareRippleNode(interactionSource, bounded, radius, colorProducer) 217 | } 218 | 219 | override fun equals(other: Any?): Boolean { 220 | if (this === other) return true 221 | if (other !is RippleNodeFactory) return false 222 | 223 | if (bounded != other.bounded) return false 224 | if (radius != other.radius) return false 225 | if (colorProducer != other.colorProducer) return false 226 | return color == other.color 227 | } 228 | 229 | override fun hashCode(): Int { 230 | var result = bounded.hashCode() 231 | result = 31 * result + radius.hashCode() 232 | result = 31 * result + colorProducer.hashCode() 233 | result = 31 * result + color.hashCode() 234 | return result 235 | } 236 | } 237 | 238 | private class DelegatingThemeAwareRippleNode( 239 | private val interactionSource: InteractionSource, 240 | private val bounded: Boolean, 241 | private val radius: Dp, 242 | private val color: ColorProducer, 243 | ) : DelegatingNode(), CompositionLocalConsumerModifierNode, ObserverModifierNode { 244 | private var rippleNode: DelegatableNode? = null 245 | 246 | override fun onAttach() { 247 | updateConfiguration() 248 | } 249 | 250 | override fun onObservedReadsChanged() { 251 | updateConfiguration() 252 | } 253 | 254 | /** 255 | * Handles [LocalRippleConfiguration] changing between null / non-null. Changes to 256 | * [RippleConfiguration.color] and [RippleConfiguration.rippleAlpha] are handled as part of the 257 | * ripple definition. 258 | */ 259 | private fun updateConfiguration() { 260 | observeReads { 261 | val configuration = currentValueOf(LocalRippleConfiguration) 262 | if (configuration == null) { 263 | removeRipple() 264 | } else { 265 | if (rippleNode == null) attachNewRipple() 266 | } 267 | } 268 | } 269 | 270 | private fun attachNewRipple() { 271 | val calculateColor = ColorProducer { 272 | val userDefinedColor = color() 273 | if (userDefinedColor.isSpecified) { 274 | userDefinedColor 275 | } else { 276 | // If this is null, the ripple will be removed, so this should always be non-null in 277 | // normal use 278 | val rippleConfiguration = currentValueOf(LocalRippleConfiguration) 279 | if (rippleConfiguration?.color?.isSpecified == true) { 280 | rippleConfiguration.color 281 | } else { 282 | Color.Unspecified 283 | } 284 | } 285 | } 286 | 287 | val calculateRippleAlpha = { 288 | // If this is null, the ripple will be removed, so this should always be non-null in 289 | // normal use 290 | val rippleConfiguration = currentValueOf(LocalRippleConfiguration) 291 | rippleConfiguration?.rippleAlpha ?: RippleDefaults.RippleAlpha 292 | } 293 | 294 | rippleNode = 295 | delegate( 296 | createRippleModifierNode( 297 | interactionSource, 298 | bounded, 299 | radius, 300 | calculateColor, 301 | calculateRippleAlpha 302 | ) 303 | ) 304 | } 305 | 306 | private fun removeRipple() { 307 | rippleNode?.let { undelegate(it) } 308 | rippleNode = null 309 | } 310 | } 311 | 312 | private val DefaultBoundedRipple = 313 | RippleNodeFactory(bounded = true, radius = Dp.Unspecified, color = Color.Unspecified) 314 | private val DefaultUnboundedRipple = 315 | RippleNodeFactory(bounded = false, radius = Dp.Unspecified, color = Color.Unspecified) 316 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/material3/tokens/StateTokens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // VERSION: v0_210 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | package androidx.compose.material3.tokens 19 | 20 | internal object StateTokens { 21 | const val DraggedStateLayerOpacity = 0.16f 22 | const val FocusStateLayerOpacity = 0.1f 23 | const val HoverStateLayerOpacity = 0.08f 24 | const val PressedStateLayerOpacity = 0.1f 25 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Bubbles 3 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/BottomNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.selection.selectableGroup 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun BottomNavigation( 19 | modifier: Modifier = Modifier, 20 | contentColor: Color = LocalOnSurfaceColor.current, 21 | backgroundColor: Color = LocalSurfaceColor.current, 22 | contentPadding: PaddingValues = PaddingValues(0.dp), 23 | content: @Composable RowScope.() -> Unit 24 | ) { 25 | Row( 26 | modifier = modifier.background(backgroundColor).padding(contentPadding).selectableGroup(), 27 | verticalAlignment = Alignment.CenterVertically, 28 | horizontalArrangement = Arrangement.SpaceEvenly, 29 | ) { 30 | CompositionLocalProvider(LocalContentColor provides contentColor) { 31 | content() 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun TabItem( 38 | selected: Boolean, 39 | onSelected: () -> Unit, 40 | modifier: Modifier = Modifier, 41 | enabled: Boolean = true, 42 | selectedColor: Color = LocalPrimaryColor.current, 43 | contentColor: Color = LocalContentColor.current, 44 | content: @Composable () -> Unit 45 | ) { 46 | GhostButton( 47 | enabled = enabled, 48 | modifier = modifier, 49 | onClick = onSelected, 50 | contentColor = if (selected) selectedColor else contentColor, 51 | ) { 52 | content() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Buttons.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.LocalIndication 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.PaddingValues 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.CompositionLocalProvider 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.draw.shadow 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.RectangleShape 21 | import androidx.compose.ui.graphics.Shape 22 | import androidx.compose.ui.graphics.isUnspecified 23 | import androidx.compose.ui.semantics.Role 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | 27 | val DefaultButtonPaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp) 28 | val DefaultButtonBorderWidth = 1.dp 29 | 30 | @Composable 31 | fun PrimaryButton( 32 | onClick: () -> Unit, 33 | enabled: Boolean = true, 34 | modifier: Modifier = Modifier, 35 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 36 | contentColor: Color = LocalOnPrimaryColor.current, 37 | backgroundColor: Color = LocalPrimaryColor.current, 38 | borderColor: Color = LocalPrimaryColor.current, 39 | borderWidth: Dp = DefaultButtonBorderWidth, 40 | elevation: Dp = 0.dp, 41 | contentPadding: PaddingValues = DefaultButtonPaddingValues, 42 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 43 | content: @Composable () -> Unit 44 | ) { 45 | val backgroundColor = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.50f) 46 | val borderColor = if (enabled) borderColor else borderColor.copy(alpha = 0.50f) 47 | 48 | UnstyledButton( 49 | onClick = onClick, 50 | enabled = enabled, 51 | shape = shape, 52 | backgroundColor = backgroundColor, 53 | modifier = modifier, 54 | contentPadding = contentPadding, 55 | borderColor = borderColor, 56 | elevation = elevation, 57 | borderWidth = borderWidth, 58 | interactionSource = interactionSource, 59 | ) { 60 | CompositionLocalProvider(LocalContentColor provides contentColor) { 61 | content() 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | fun SecondaryButton( 68 | onClick: () -> Unit, 69 | modifier: Modifier = Modifier, 70 | enabled: Boolean = true, 71 | contentColor: Color = LocalOnSecondaryColor.current, 72 | backgroundColor: Color = LocalSecondaryColor.current, 73 | borderColor: Color = LocalSecondaryColor.current, 74 | borderWidth: Dp = DefaultButtonBorderWidth, 75 | elevation: Dp = 0.dp, 76 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 77 | contentPadding: PaddingValues = DefaultButtonPaddingValues, 78 | content: @Composable () -> Unit 79 | ) { 80 | val contentColor = if (enabled) contentColor else contentColor.copy(alpha = 0.50f) 81 | val backgroundColor = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.50f) 82 | val borderColor = if (enabled) borderColor else borderColor.copy(alpha = 0.50f) 83 | 84 | UnstyledButton( 85 | enabled = enabled, 86 | onClick = onClick, 87 | shape = shape, 88 | backgroundColor = backgroundColor, 89 | modifier = modifier, 90 | borderColor = borderColor, 91 | borderWidth = borderWidth, 92 | elevation = elevation, 93 | contentPadding = contentPadding, 94 | ) { 95 | CompositionLocalProvider(LocalContentColor provides contentColor) { 96 | content() 97 | } 98 | } 99 | } 100 | 101 | @Composable 102 | fun OutlinedButton( 103 | onClick: () -> Unit, 104 | modifier: Modifier = Modifier, 105 | enabled: Boolean = true, 106 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 107 | contentColor: Color = LocalContentColor.current, 108 | backgroundColor: Color = Color.Transparent, 109 | borderColor: Color = LocalOutlineColor.current, 110 | borderWidth: Dp = DefaultButtonBorderWidth, 111 | contentPadding: PaddingValues = DefaultButtonPaddingValues, 112 | elevation: Dp = 0.dp, 113 | content: @Composable () -> Unit 114 | ) { 115 | val contentColor = if (enabled) contentColor else contentColor.copy(alpha = 0.50f) 116 | val backgroundColor = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.50f) 117 | val borderColor = if (enabled) borderColor else borderColor.copy(alpha = 0.50f) 118 | 119 | UnstyledButton( 120 | onClick = onClick, 121 | shape = shape, 122 | enabled = enabled, 123 | backgroundColor = backgroundColor, 124 | modifier = modifier, 125 | contentPadding = contentPadding, 126 | elevation = elevation, 127 | borderWidth = borderWidth, 128 | borderColor = borderColor, 129 | ) { 130 | CompositionLocalProvider(LocalContentColor provides contentColor) { 131 | content() 132 | } 133 | } 134 | } 135 | 136 | 137 | @Composable 138 | fun GhostButton( 139 | onClick: () -> Unit, 140 | enabled: Boolean = true, 141 | modifier: Modifier = Modifier, 142 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 143 | contentColor: Color = LocalContentColor.current, 144 | backgroundColor: Color = Color.Transparent, 145 | borderColor: Color = Color.Transparent, 146 | borderWidth: Dp = DefaultButtonBorderWidth, 147 | contentPadding: PaddingValues = DefaultButtonPaddingValues, 148 | content: @Composable () -> Unit 149 | ) { 150 | val contentColor = if (enabled) contentColor else contentColor.copy(alpha = 0.50f) 151 | val borderColor = when { 152 | borderColor.isUnspecified || borderColor == Color.Transparent -> Color.Transparent 153 | enabled -> borderColor 154 | else -> borderColor.copy(alpha = 0.50f) 155 | } 156 | UnstyledButton( 157 | onClick = onClick, 158 | shape = shape, 159 | backgroundColor = backgroundColor, 160 | modifier = modifier, 161 | enabled = enabled, 162 | borderColor = borderColor, 163 | borderWidth = borderWidth, 164 | contentPadding = contentPadding, 165 | ) { 166 | CompositionLocalProvider(LocalContentColor provides contentColor) { 167 | content() 168 | } 169 | } 170 | } 171 | 172 | @Composable 173 | fun DestructiveButton( 174 | onClick: () -> Unit, 175 | enabled: Boolean = true, 176 | modifier: Modifier = Modifier, 177 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 178 | contentColor: Color = LocalOnDestructiveColor.current, 179 | backgroundColor: Color = LocalDestructiveColor.current, 180 | borderColor: Color = LocalDestructiveColor.current, 181 | borderWidth: Dp = DefaultButtonBorderWidth, 182 | contentPadding: PaddingValues = DefaultButtonPaddingValues, 183 | elevation: Dp = 0.dp, 184 | content: @Composable () -> Unit 185 | ) { 186 | val backgroundColor = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.50f) 187 | val borderColor = if (enabled) borderColor else borderColor.copy(alpha = 0.50f) 188 | 189 | UnstyledButton( 190 | onClick = onClick, 191 | shape = shape, 192 | backgroundColor = backgroundColor, 193 | modifier = modifier, 194 | enabled = enabled, 195 | borderColor = borderColor, 196 | elevation = elevation, 197 | borderWidth = borderWidth, 198 | contentPadding = contentPadding, 199 | ) { 200 | CompositionLocalProvider(LocalContentColor provides contentColor) { 201 | content() 202 | } 203 | } 204 | } 205 | 206 | @Composable 207 | private fun UnstyledButton( 208 | onClick: () -> Unit, 209 | enabled: Boolean = true, 210 | shape: Shape = RectangleShape, 211 | backgroundColor: Color = Color.Unspecified, 212 | elevation: Dp = 0.dp, 213 | contentPadding: PaddingValues = PaddingValues(0.dp), 214 | borderColor: Color = Color.Unspecified, 215 | borderWidth: Dp = 0.dp, 216 | modifier: Modifier = Modifier, 217 | indication: Indication? = LocalIndication.current, 218 | interactionSource: MutableInteractionSource? = null, 219 | content: @Composable () -> Unit = {} 220 | ) { 221 | Box( 222 | modifier = modifier 223 | .then(if (elevation > 0.dp) Modifier.shadow(elevation, shape) else Modifier) 224 | .clip(shape) 225 | .background(backgroundColor) 226 | .clickable( 227 | onClick = onClick, 228 | role = Role.Button, 229 | enabled = enabled, 230 | indication = indication, 231 | interactionSource = interactionSource 232 | ) 233 | .border(borderWidth, borderColor, shape) 234 | .padding(contentPadding) 235 | ) { 236 | content() 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Card.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.draw.shadow 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.Shape 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.zIndex 19 | 20 | @Composable 21 | fun Card( 22 | modifier: Modifier = Modifier, 23 | contentPadding: PaddingValues = PaddingValues(0.dp), 24 | elevation: Dp = 0.dp, 25 | backgroundColor: Color = LocalSurfaceColor.current, 26 | contentColor: Color = LocalOnSurfaceColor.current, 27 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 28 | borderWidth: Dp = 0.dp, 29 | borderColor: Color = LocalOutlineColor.current, 30 | content: @Composable () -> Unit = {} 31 | ) { 32 | Box( 33 | modifier 34 | .shadow(elevation = elevation, shape = shape) 35 | .zIndex(elevation.value) 36 | .border(width = borderWidth, color = borderColor, shape = shape) 37 | .clip(shape) 38 | .background(backgroundColor, shape) 39 | .padding(contentPadding) 40 | ) { 41 | CompositionLocalProvider(LocalContentColor provides contentColor) { 42 | content() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/CheckBox.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.LocalIndication 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.Shape 18 | import androidx.compose.ui.semantics.Role 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.dp 21 | import com.composables.core.Icon 22 | import com.composables.icons.lucide.Check 23 | import com.composables.icons.lucide.Lucide 24 | 25 | @Composable 26 | fun Checkbox( 27 | checked: Boolean, 28 | modifier: Modifier = Modifier, 29 | backgroundColor: Color = Color.Transparent, 30 | contentColor: Color = LocalContentColor.current, 31 | enabled: Boolean = true, 32 | onCheckedChange: ((Boolean) -> Unit)? = null, 33 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 34 | borderColor: Color = LocalOutlineColor.current, 35 | borderWidth: Dp = 1.dp, 36 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 37 | checkIcon: @Composable () -> Unit, 38 | ) { 39 | val checkboxAlpha = if (checked) 1f else 0f 40 | 41 | Box( 42 | modifier 43 | .border(borderWidth, borderColor, shape) 44 | .clip(shape) 45 | .background(backgroundColor) 46 | then if (onCheckedChange != null) { 47 | Modifier.clickable( 48 | enabled = enabled, 49 | indication = LocalIndication.current, 50 | interactionSource = interactionSource, 51 | role = Role.Checkbox, 52 | ) { 53 | onCheckedChange(checked.not()) 54 | } 55 | } else { 56 | Modifier 57 | } 58 | ) { 59 | Box(modifier = Modifier.alpha(checkboxAlpha)) { 60 | CompositionLocalProvider(LocalContentColor provides contentColor) { 61 | checkIcon() 62 | } 63 | } 64 | } 65 | } 66 | 67 | 68 | @Composable 69 | fun Check() { 70 | Icon(Lucide.Check, contentDescription = null, tint = LocalContentColor.current) 71 | } 72 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/ContainerSize.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.unit.DpSize 5 | 6 | @Composable 7 | expect fun currentContainerSize(): DpSize 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | @Composable 7 | inline fun buildModifier(builder: @Composable MutableList.() -> Unit): Modifier { 8 | val list = mutableListOf() 9 | builder(list) 10 | return list.fold(Modifier as Modifier) { acc, modifier -> 11 | acc then modifier 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxHeight 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.heightIn 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.Shape 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun ProgressBar( 23 | progress: Float, 24 | modifier: Modifier = Modifier, 25 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 26 | color: Color = LocalPrimaryColor.current, 27 | backgroundColor: Color = LocalSecondaryColor.current, 28 | animationSpec: AnimationSpec = tween(easing = LinearEasing), 29 | ) { 30 | val cappedProgress = progress.coerceIn(0f, 1f) 31 | val animatedProgress by animateFloatAsState(targetValue = cappedProgress, animationSpec = animationSpec) 32 | 33 | Box(modifier.clip(shape).background(backgroundColor).heightIn(min = 2.dp)) { 34 | Box(Modifier.matchParentSize()) { 35 | Box( 36 | Modifier 37 | .background(color) 38 | .fillMaxHeight() 39 | .fillMaxWidth(animatedProgress) 40 | ) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/RadioButton.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.LocalIndication 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.PaddingValues 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.CircleShape 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.CompositionLocalProvider 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.Shape 21 | import androidx.compose.ui.semantics.Role 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | 25 | @Composable 26 | fun RadioButton( 27 | selected: Boolean, 28 | modifier: Modifier = Modifier, 29 | backgroundColor: Color = LocalSurfaceColor.current, 30 | contentColor: Color = LocalOnSurfaceColor.current, 31 | selectedColor: Color = LocalPrimaryColor.current, 32 | enabled: Boolean = true, 33 | onSelectedChange: ((Boolean) -> Unit)? = null, 34 | shape: Shape = CircleShape, 35 | borderColor: Color = LocalOutlineColor.current, 36 | borderWidth: Dp = 1.dp, 37 | contentPadding: PaddingValues = PaddingValues(0.dp), 38 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 39 | radio: @Composable () -> Unit, 40 | ) { 41 | val checkboxAlpha = if (selected) 1f else 0f 42 | 43 | Box( 44 | modifier 45 | .border(borderWidth, borderColor, shape) 46 | .clip(shape) 47 | .background(backgroundColor) 48 | then if (onSelectedChange != null) { 49 | Modifier.clickable( 50 | enabled = enabled, 51 | indication = LocalIndication.current, 52 | interactionSource = interactionSource, 53 | role = Role.RadioButton, 54 | ) { 55 | onSelectedChange(selected.not()) 56 | } 57 | } else { 58 | Modifier 59 | }.padding(contentPadding) 60 | ) { 61 | Box(modifier = Modifier.alpha(checkboxAlpha)) { 62 | CompositionLocalProvider( 63 | LocalContentColor provides if (selected) selectedColor else contentColor 64 | ) { 65 | radio() 66 | } 67 | } 68 | } 69 | } 70 | 71 | @Composable 72 | fun Radio() { 73 | Box( 74 | modifier = Modifier 75 | .size(24.dp) 76 | .padding(4.dp) 77 | .clip(CircleShape) 78 | .background(LocalPrimaryColor.current) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/ScreenWidthBreakpoint.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalComposeUiApi::class) 2 | 3 | package com.builtwithpaper 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.ExperimentalComposeUiApi 7 | import androidx.compose.ui.unit.Dp 8 | import androidx.compose.ui.unit.dp 9 | 10 | sealed class ScreenWidthBreakpoint(val minWidth: Dp) { 11 | object Default : ScreenWidthBreakpoint(0.dp) 12 | object Small : ScreenWidthBreakpoint(640.dp) 13 | object Medium : ScreenWidthBreakpoint(768.dp) 14 | object Large : ScreenWidthBreakpoint(1024.dp) 15 | object ExtraLarge : ScreenWidthBreakpoint(1280.dp) 16 | object ExtraExtraLarge : ScreenWidthBreakpoint(1536.dp) 17 | } 18 | 19 | /** 20 | * Calculates the current [ScreenWidthBreakpoint] according to the current application's container width. 21 | */ 22 | @Composable 23 | fun currentScreenWidthBreakpoint(): ScreenWidthBreakpoint { 24 | val screenWidth = currentContainerSize().width 25 | 26 | val breakpoint = when { 27 | screenWidth > 1536.dp -> ScreenWidthBreakpoint.ExtraExtraLarge 28 | screenWidth > 1280.dp -> ScreenWidthBreakpoint.ExtraLarge 29 | screenWidth > 1024.dp -> ScreenWidthBreakpoint.Large 30 | screenWidth > 768.dp -> ScreenWidthBreakpoint.Medium 31 | screenWidth > 640.dp -> ScreenWidthBreakpoint.Small 32 | else -> ScreenWidthBreakpoint.Default 33 | } 34 | return breakpoint 35 | } 36 | 37 | infix fun ScreenWidthBreakpoint.isAtLeast(other: ScreenWidthBreakpoint): Boolean { 38 | return minWidth >= other.minWidth 39 | } 40 | 41 | infix fun ScreenWidthBreakpoint.isAtMost(other: ScreenWidthBreakpoint): Boolean { 42 | return minWidth <= other.minWidth 43 | } 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Slider.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.Orientation 5 | import androidx.compose.foundation.gestures.draggable 6 | import androidx.compose.foundation.gestures.rememberDraggableState 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.shadow 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.Shape 25 | import androidx.compose.ui.layout.onPlaced 26 | import androidx.compose.ui.platform.LocalDensity 27 | import androidx.compose.ui.unit.Dp 28 | import androidx.compose.ui.unit.dp 29 | 30 | @Composable 31 | fun Slider( 32 | value: Float, 33 | onValueChange: (Float) -> Unit, 34 | modifier: Modifier = Modifier, 35 | enabled: Boolean = true, 36 | trackColor: Color = LocalPrimaryColor.current, 37 | trackShape: Shape = RoundedCornerShape(LocalCornerRadius.current), 38 | thumb: @Composable () -> Unit 39 | ) { 40 | var thumbWidth by remember { mutableStateOf(0.dp) } 41 | var trackWidth by remember { mutableStateOf(0) } 42 | 43 | val density = LocalDensity.current 44 | 45 | val thumbPosition = trackWidth * value 46 | 47 | Box(modifier = modifier, contentAlignment = Alignment.CenterStart) { 48 | Box( 49 | Modifier 50 | .fillMaxWidth() 51 | .padding(horizontal = thumbWidth / 2) 52 | .height(4.dp) 53 | .background(trackColor.copy(0.44f), trackShape) 54 | .onPlaced { 55 | trackWidth = with(density) { it.size.width } 56 | } 57 | ) 58 | Box( 59 | Modifier 60 | .padding(horizontal = thumbWidth / 2) 61 | .width(with(density) { thumbPosition.toDp() }) 62 | .height(4.dp) 63 | .background(trackColor, trackShape) 64 | ) 65 | 66 | Box( 67 | Modifier 68 | .onPlaced { thumbWidth = with(density) { it.size.width.toDp() } } 69 | .offset(x = with(density) { thumbPosition.toDp() }) 70 | .draggable( 71 | enabled = enabled, 72 | orientation = Orientation.Horizontal, 73 | state = rememberDraggableState { deltaPx -> 74 | val valueDelta = deltaPx / trackWidth 75 | val newValue = (value + valueDelta).coerceIn(0f, 1f) 76 | onValueChange(newValue) 77 | } 78 | ) 79 | ) { 80 | thumb() 81 | } 82 | } 83 | } 84 | 85 | 86 | @Composable 87 | fun SliderThumb( 88 | color: Color = LocalSurfaceColor.current, 89 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 90 | elevation: Dp = 4.dp, 91 | ) { 92 | Box( 93 | Modifier 94 | .size(28.dp) 95 | .shadow(elevation, shape) 96 | .background(color, shape) 97 | ) 98 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/SocialIcons.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.graphics.StrokeJoin 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | 13 | public val Google: ImageVector 14 | get() { 15 | if (_Google != null) { 16 | return _Google!! 17 | } 18 | _Google = ImageVector.Builder( 19 | name = "Google", 20 | defaultWidth = 24.dp, 21 | defaultHeight = 24.dp, 22 | viewportWidth = 16f, 23 | viewportHeight = 16f 24 | ).apply { 25 | path( 26 | fill = SolidColor(Color(0xFF000000)), 27 | fillAlpha = 1.0f, 28 | stroke = null, 29 | strokeAlpha = 1.0f, 30 | strokeLineWidth = 1.0f, 31 | strokeLineCap = StrokeCap.Butt, 32 | strokeLineJoin = StrokeJoin.Miter, 33 | strokeLineMiter = 1.0f, 34 | pathFillType = PathFillType.NonZero 35 | ) { 36 | moveTo(15.545f, 6.558f) 37 | arcToRelative(9.4f, 9.4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.139f, 1.626f) 38 | curveToRelative(0f, 2.434f, -0.87f, 4.492f, -2.384f, 5.885f) 39 | horizontalLineToRelative(0.002f) 40 | curveTo(11.978f, 15.292f, 10.158f, 16f, 8f, 16f) 41 | arcTo(8f, 8f, 0f, isMoreThanHalf = true, isPositiveArc = true, 8f, 0f) 42 | arcToRelative(7.7f, 7.7f, 0f, isMoreThanHalf = false, isPositiveArc = true, 5.352f, 2.082f) 43 | lineToRelative(-2.284f, 2.284f) 44 | arcTo(4.35f, 4.35f, 0f, isMoreThanHalf = false, isPositiveArc = false, 8f, 3.166f) 45 | curveToRelative(-2.087f, 0f, -3.86f, 1.408f, -4.492f, 3.304f) 46 | arcToRelative(4.8f, 4.8f, 0f, isMoreThanHalf = false, isPositiveArc = false, 0f, 3.063f) 47 | horizontalLineToRelative(0.003f) 48 | curveToRelative(0.635f, 1.893f, 2.405f, 3.301f, 4.492f, 3.301f) 49 | curveToRelative(1.078f, 0f, 2.004f, -0.276f, 2.722f, -0.764f) 50 | horizontalLineToRelative(-0.003f) 51 | arcToRelative(3.7f, 3.7f, 0f, isMoreThanHalf = false, isPositiveArc = false, 1.599f, -2.431f) 52 | horizontalLineTo(8f) 53 | verticalLineToRelative(-3.08f) 54 | close() 55 | } 56 | }.build() 57 | return _Google!! 58 | } 59 | 60 | private var _Google: ImageVector? = null 61 | 62 | 63 | public val Github: ImageVector 64 | get() { 65 | if (_Github != null) { 66 | return _Github!! 67 | } 68 | _Github = ImageVector.Builder( 69 | name = "Github", 70 | defaultWidth = 24.dp, 71 | defaultHeight = 24.dp, 72 | viewportWidth = 16f, 73 | viewportHeight = 16f 74 | ).apply { 75 | path( 76 | fill = SolidColor(Color(0xFF000000)), 77 | fillAlpha = 1.0f, 78 | stroke = null, 79 | strokeAlpha = 1.0f, 80 | strokeLineWidth = 1.0f, 81 | strokeLineCap = StrokeCap.Butt, 82 | strokeLineJoin = StrokeJoin.Miter, 83 | strokeLineMiter = 1.0f, 84 | pathFillType = PathFillType.NonZero 85 | ) { 86 | moveTo(8f, 0f) 87 | curveTo(3.58f, 0f, 0f, 3.58f, 0f, 8f) 88 | curveToRelative(0f, 3.54f, 2.29f, 6.53f, 5.47f, 7.59f) 89 | curveToRelative(0.4f, 0.07f, 0.55f, -0.17f, 0.55f, -0.38f) 90 | curveToRelative(0f, -0.19f, -0.01f, -0.82f, -0.01f, -1.49f) 91 | curveToRelative(-2.01f, 0.37f, -2.53f, -0.49f, -2.69f, -0.94f) 92 | curveToRelative(-0.09f, -0.23f, -0.48f, -0.94f, -0.82f, -1.13f) 93 | curveToRelative(-0.28f, -0.15f, -0.68f, -0.52f, -0.01f, -0.53f) 94 | curveToRelative(0.63f, -0.01f, 1.08f, 0.58f, 1.23f, 0.82f) 95 | curveToRelative(0.72f, 1.21f, 1.87f, 0.87f, 2.33f, 0.66f) 96 | curveToRelative(0.07f, -0.52f, 0.28f, -0.87f, 0.51f, -1.07f) 97 | curveToRelative(-1.78f, -0.2f, -3.64f, -0.89f, -3.64f, -3.95f) 98 | curveToRelative(0f, -0.87f, 0.31f, -1.59f, 0.82f, -2.15f) 99 | curveToRelative(-0.08f, -0.2f, -0.36f, -1.02f, 0.08f, -2.12f) 100 | curveToRelative(0f, 0f, 0.67f, -0.21f, 2.2f, 0.82f) 101 | curveToRelative(0.64f, -0.18f, 1.32f, -0.27f, 2f, -0.27f) 102 | reflectiveCurveToRelative(1.36f, 0.09f, 2f, 0.27f) 103 | curveToRelative(1.53f, -1.04f, 2.2f, -0.82f, 2.2f, -0.82f) 104 | curveToRelative(0.44f, 1.1f, 0.16f, 1.92f, 0.08f, 2.12f) 105 | curveToRelative(0.51f, 0.56f, 0.82f, 1.27f, 0.82f, 2.15f) 106 | curveToRelative(0f, 3.07f, -1.87f, 3.75f, -3.65f, 3.95f) 107 | curveToRelative(0.29f, 0.25f, 0.54f, 0.73f, 0.54f, 1.48f) 108 | curveToRelative(0f, 1.07f, -0.01f, 1.93f, -0.01f, 2.2f) 109 | curveToRelative(0f, 0.21f, 0.15f, 0.46f, 0.55f, 0.38f) 110 | arcTo(8.01f, 8.01f, 0f, isMoreThanHalf = false, isPositiveArc = false, 16f, 8f) 111 | curveToRelative(0f, -4.42f, -3.58f, -8f, -8f, -8f) 112 | } 113 | }.build() 114 | return _Github!! 115 | } 116 | 117 | private var _Github: ImageVector? = null 118 | 119 | 120 | public val Facebook: ImageVector 121 | get() { 122 | if (_Facebook != null) { 123 | return _Facebook!! 124 | } 125 | _Facebook = ImageVector.Builder( 126 | name = "Facebook", 127 | defaultWidth = 24.dp, 128 | defaultHeight = 24.dp, 129 | viewportWidth = 16f, 130 | viewportHeight = 16f 131 | ).apply { 132 | path( 133 | fill = SolidColor(Color(0xFF000000)), 134 | fillAlpha = 1.0f, 135 | stroke = null, 136 | strokeAlpha = 1.0f, 137 | strokeLineWidth = 1.0f, 138 | strokeLineCap = StrokeCap.Butt, 139 | strokeLineJoin = StrokeJoin.Miter, 140 | strokeLineMiter = 1.0f, 141 | pathFillType = PathFillType.NonZero 142 | ) { 143 | moveTo(16f, 8.049f) 144 | curveToRelative(0f, -4.446f, -3.582f, -8.05f, -8f, -8.05f) 145 | curveTo(3.58f, 0f, -0.002f, 3.603f, -0.002f, 8.05f) 146 | curveToRelative(0f, 4.017f, 2.926f, 7.347f, 6.75f, 7.951f) 147 | verticalLineToRelative(-5.625f) 148 | horizontalLineToRelative(-2.03f) 149 | verticalLineTo(8.05f) 150 | horizontalLineTo(6.75f) 151 | verticalLineTo(6.275f) 152 | curveToRelative(0f, -2.017f, 1.195f, -3.131f, 3.022f, -3.131f) 153 | curveToRelative(0.876f, 0f, 1.791f, 0.157f, 1.791f, 0.157f) 154 | verticalLineToRelative(1.98f) 155 | horizontalLineToRelative(-1.009f) 156 | curveToRelative(-0.993f, 0f, -1.303f, 0.621f, -1.303f, 1.258f) 157 | verticalLineToRelative(1.51f) 158 | horizontalLineToRelative(2.218f) 159 | lineToRelative(-0.354f, 2.326f) 160 | horizontalLineTo(9.25f) 161 | verticalLineTo(16f) 162 | curveToRelative(3.824f, -0.604f, 6.75f, -3.934f, 6.75f, -7.951f) 163 | } 164 | }.build() 165 | return _Facebook!! 166 | } 167 | 168 | private var _Facebook: ImageVector? = null 169 | 170 | public val Apple: ImageVector 171 | get() { 172 | if (_Apple != null) { 173 | return _Apple!! 174 | } 175 | _Apple = ImageVector.Builder( 176 | name = "Apple", 177 | defaultWidth = 24.dp, 178 | defaultHeight = 24.dp, 179 | viewportWidth = 16f, 180 | viewportHeight = 16f 181 | ).apply { 182 | path( 183 | fill = SolidColor(Color(0xFF000000)), 184 | fillAlpha = 1.0f, 185 | stroke = null, 186 | strokeAlpha = 1.0f, 187 | strokeLineWidth = 1.0f, 188 | strokeLineCap = StrokeCap.Butt, 189 | strokeLineJoin = StrokeJoin.Miter, 190 | strokeLineMiter = 1.0f, 191 | pathFillType = PathFillType.NonZero 192 | ) { 193 | moveTo(11.182f, 0.008f) 194 | curveTo(11.148f, -0.03f, 9.923f, 0.023f, 8.857f, 1.18f) 195 | curveToRelative(-1.066f, 1.156f, -0.902f, 2.482f, -0.878f, 2.516f) 196 | reflectiveCurveToRelative(1.52f, 0.087f, 2.475f, -1.258f) 197 | reflectiveCurveToRelative(0.762f, -2.391f, 0.728f, -2.43f) 198 | moveToRelative(3.314f, 11.733f) 199 | curveToRelative(-0.048f, -0.096f, -2.325f, -1.234f, -2.113f, -3.422f) 200 | reflectiveCurveToRelative(1.675f, -2.789f, 1.698f, -2.854f) 201 | reflectiveCurveToRelative(-0.597f, -0.79f, -1.254f, -1.157f) 202 | arcToRelative(3.7f, 3.7f, 0f, isMoreThanHalf = false, isPositiveArc = false, -1.563f, -0.434f) 203 | curveToRelative(-0.108f, -0.003f, -0.483f, -0.095f, -1.254f, 0.116f) 204 | curveToRelative(-0.508f, 0.139f, -1.653f, 0.589f, -1.968f, 0.607f) 205 | curveToRelative(-0.316f, 0.018f, -1.256f, -0.522f, -2.267f, -0.665f) 206 | curveToRelative(-0.647f, -0.125f, -1.333f, 0.131f, -1.824f, 0.328f) 207 | curveToRelative(-0.49f, 0.196f, -1.422f, 0.754f, -2.074f, 2.237f) 208 | curveToRelative(-0.652f, 1.482f, -0.311f, 3.83f, -0.067f, 4.56f) 209 | reflectiveCurveToRelative(0.625f, 1.924f, 1.273f, 2.796f) 210 | curveToRelative(0.576f, 0.984f, 1.34f, 1.667f, 1.659f, 1.899f) 211 | reflectiveCurveToRelative(1.219f, 0.386f, 1.843f, 0.067f) 212 | curveToRelative(0.502f, -0.308f, 1.408f, -0.485f, 1.766f, -0.472f) 213 | curveToRelative(0.357f, 0.013f, 1.061f, 0.154f, 1.782f, 0.539f) 214 | curveToRelative(0.571f, 0.197f, 1.111f, 0.115f, 1.652f, -0.105f) 215 | curveToRelative(0.541f, -0.221f, 1.324f, -1.059f, 2.238f, -2.758f) 216 | quadToRelative(0.52f, -1.185f, 0.473f, -1.282f) 217 | } 218 | path( 219 | fill = SolidColor(Color(0xFF000000)), 220 | fillAlpha = 1.0f, 221 | stroke = null, 222 | strokeAlpha = 1.0f, 223 | strokeLineWidth = 1.0f, 224 | strokeLineCap = StrokeCap.Butt, 225 | strokeLineJoin = StrokeJoin.Miter, 226 | strokeLineMiter = 1.0f, 227 | pathFillType = PathFillType.NonZero 228 | ) { 229 | moveTo(11.182f, 0.008f) 230 | curveTo(11.148f, -0.03f, 9.923f, 0.023f, 8.857f, 1.18f) 231 | curveToRelative(-1.066f, 1.156f, -0.902f, 2.482f, -0.878f, 2.516f) 232 | reflectiveCurveToRelative(1.52f, 0.087f, 2.475f, -1.258f) 233 | reflectiveCurveToRelative(0.762f, -2.391f, 0.728f, -2.43f) 234 | moveToRelative(3.314f, 11.733f) 235 | curveToRelative(-0.048f, -0.096f, -2.325f, -1.234f, -2.113f, -3.422f) 236 | reflectiveCurveToRelative(1.675f, -2.789f, 1.698f, -2.854f) 237 | reflectiveCurveToRelative(-0.597f, -0.79f, -1.254f, -1.157f) 238 | arcToRelative(3.7f, 3.7f, 0f, isMoreThanHalf = false, isPositiveArc = false, -1.563f, -0.434f) 239 | curveToRelative(-0.108f, -0.003f, -0.483f, -0.095f, -1.254f, 0.116f) 240 | curveToRelative(-0.508f, 0.139f, -1.653f, 0.589f, -1.968f, 0.607f) 241 | curveToRelative(-0.316f, 0.018f, -1.256f, -0.522f, -2.267f, -0.665f) 242 | curveToRelative(-0.647f, -0.125f, -1.333f, 0.131f, -1.824f, 0.328f) 243 | curveToRelative(-0.49f, 0.196f, -1.422f, 0.754f, -2.074f, 2.237f) 244 | curveToRelative(-0.652f, 1.482f, -0.311f, 3.83f, -0.067f, 4.56f) 245 | reflectiveCurveToRelative(0.625f, 1.924f, 1.273f, 2.796f) 246 | curveToRelative(0.576f, 0.984f, 1.34f, 1.667f, 1.659f, 1.899f) 247 | reflectiveCurveToRelative(1.219f, 0.386f, 1.843f, 0.067f) 248 | curveToRelative(0.502f, -0.308f, 1.408f, -0.485f, 1.766f, -0.472f) 249 | curveToRelative(0.357f, 0.013f, 1.061f, 0.154f, 1.782f, 0.539f) 250 | curveToRelative(0.571f, 0.197f, 1.111f, 0.115f, 1.652f, -0.105f) 251 | curveToRelative(0.541f, -0.221f, 1.324f, -1.059f, 2.238f, -2.758f) 252 | quadToRelative(0.52f, -1.185f, 0.473f, -1.282f) 253 | } 254 | }.build() 255 | return _Apple!! 256 | } 257 | 258 | private var _Apple: ImageVector? = null 259 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Text.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.text.BasicText 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.AnnotatedString 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.text.font.FontFamily 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.text.style.TextAlign 15 | import androidx.compose.ui.text.style.TextOverflow 16 | import androidx.compose.ui.unit.TextUnit 17 | 18 | @Composable 19 | fun Text( 20 | text: String, 21 | modifier: Modifier = Modifier, 22 | style: TextStyle = LocalTextStyle.current, 23 | textAlign: TextAlign = TextAlign.Unspecified, 24 | lineHeight: TextUnit = TextUnit.Unspecified, 25 | fontSize: TextUnit = style.fontSize, 26 | fontWeight: FontWeight? = style.fontWeight, 27 | color: Color = Color.Unspecified, 28 | fontFamily: FontFamily? = style.fontFamily, 29 | singleLine: Boolean = false, 30 | minLines: Int = 1, 31 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 32 | overflow: TextOverflow = TextOverflow.Clip 33 | ) { 34 | val currentColor = listOf( 35 | color, style.color, LocalContentColor.current 36 | ).firstOrNull { it != Color.Unspecified } ?: Color.Unspecified 37 | 38 | val currentTextAlign = listOf( 39 | textAlign, 40 | style.textAlign, 41 | ).firstOrNull { it != TextAlign.Unspecified } ?: TextAlign.Unspecified 42 | 43 | val currentLineHeight = listOf( 44 | lineHeight, 45 | style.lineHeight, 46 | ).firstOrNull { it != TextUnit.Unspecified } ?: TextUnit.Unspecified 47 | 48 | val currentStyle by remember( 49 | style, 50 | currentTextAlign, 51 | fontSize, 52 | currentColor, 53 | fontWeight, 54 | fontFamily, 55 | currentLineHeight 56 | ) { 57 | mutableStateOf( 58 | style.copy( 59 | textAlign = currentTextAlign, 60 | fontSize = fontSize, 61 | color = currentColor, 62 | fontWeight = fontWeight, 63 | fontFamily = fontFamily, 64 | lineHeight = currentLineHeight 65 | ) 66 | ) 67 | } 68 | BasicText( 69 | text = text, 70 | modifier = modifier, 71 | style = currentStyle, 72 | minLines = minLines, 73 | maxLines = maxLines, 74 | overflow = overflow, 75 | ) 76 | } 77 | 78 | @Composable 79 | fun Text( 80 | text: AnnotatedString, 81 | modifier: Modifier = Modifier, 82 | style: TextStyle = LocalTextStyle.current, 83 | textAlign: TextAlign = TextAlign.Unspecified, 84 | fontSize: TextUnit = style.fontSize, 85 | fontWeight: FontWeight? = style.fontWeight, 86 | color: Color = Color.Unspecified, 87 | lineHeight: TextUnit = TextUnit.Unspecified, 88 | fontFamily: FontFamily? = style.fontFamily, 89 | singleLine: Boolean = false, 90 | minLines: Int = 1, 91 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 92 | overflow: TextOverflow = TextOverflow.Clip 93 | ) { 94 | val currentColor = listOf( 95 | color, style.color, LocalContentColor.current 96 | ).firstOrNull { it != Color.Unspecified } ?: Color.Unspecified 97 | 98 | val currentTextAlign = listOf( 99 | textAlign, 100 | style.textAlign, 101 | ).firstOrNull { it != TextAlign.Unspecified } ?: TextAlign.Unspecified 102 | 103 | val currentLineHeight = listOf( 104 | lineHeight, 105 | style.lineHeight, 106 | ).firstOrNull { it != TextUnit.Unspecified } ?: TextUnit.Unspecified 107 | 108 | val currentStyle by remember( 109 | style, 110 | currentTextAlign, 111 | fontSize, 112 | currentColor, 113 | fontWeight, 114 | fontFamily, 115 | currentLineHeight 116 | ) { 117 | mutableStateOf( 118 | style.copy( 119 | textAlign = currentTextAlign, 120 | fontSize = fontSize, 121 | color = currentColor, 122 | fontWeight = fontWeight, 123 | fontFamily = fontFamily, 124 | lineHeight = currentLineHeight 125 | ) 126 | ) 127 | } 128 | BasicText( 129 | text = text, 130 | modifier = modifier, 131 | style = currentStyle, 132 | minLines = minLines, 133 | maxLines = maxLines, 134 | overflow = overflow, 135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/TextField.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.IntrinsicSize 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.layout.widthIn 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.Shape 17 | import androidx.compose.ui.graphics.isSpecified 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontFamily 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.text.style.TextOverflow 23 | import androidx.compose.ui.unit.Dp 24 | import androidx.compose.ui.unit.TextUnit 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.isSpecified 27 | 28 | @Composable 29 | fun TextField( 30 | value: String, 31 | editable: Boolean = true, 32 | onValueChange: (String) -> Unit, 33 | modifier: Modifier = Modifier, 34 | contentPadding: PaddingValues = PaddingValues(0.dp), 35 | placeholderText: String = "", 36 | contentColor: Color = LocalContentColor.current, 37 | backgroundColor: Color = Color.Unspecified, 38 | borderWidth: Dp = 0.dp, 39 | borderColor: Color = LocalOutlineColor.current, 40 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 41 | textStyle: TextStyle = TextStyle.Default, 42 | textAlign: TextAlign = TextAlign.Unspecified, 43 | fontSize: TextUnit = TextUnit.Unspecified, 44 | fontWeight: FontWeight? = null, 45 | fontFamily: FontFamily? = null, 46 | singleLine: Boolean = false, 47 | minLines: Int = 1, 48 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 49 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 50 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 51 | overflow: TextOverflow = TextOverflow.Ellipsis 52 | ) { 53 | val overrideColorOrUnspecified = if (contentColor.isSpecified) { 54 | contentColor 55 | } else if (textStyle.color.isSpecified) { 56 | textStyle.color 57 | } else { 58 | LocalContentColor.current 59 | } 60 | 61 | val overrideTextAlign = if (textAlign != TextAlign.Unspecified) { 62 | textAlign 63 | } else textStyle.textAlign 64 | 65 | val overrideFontSize = if (fontSize != TextUnit.Unspecified) { 66 | fontSize 67 | } else textStyle.fontSize 68 | 69 | val overrideFontWeight = fontWeight ?: textStyle.fontWeight 70 | val overrideFontFamily = fontFamily ?: textStyle.fontFamily 71 | 72 | val overriddenStyle = textStyle.merge( 73 | fontWeight = overrideFontWeight, 74 | fontSize = overrideFontSize, 75 | fontFamily = overrideFontFamily, 76 | textAlign = overrideTextAlign, 77 | ) 78 | TextInput( 79 | modifier = modifier.widthIn(min = 2.dp).width(IntrinsicSize.Max) then buildModifier { 80 | if (borderWidth.isSpecified && borderWidth > 0.dp && borderColor.isSpecified) { 81 | add(Modifier.border(borderWidth, borderColor, shape)) 82 | } 83 | add( 84 | Modifier 85 | .background(backgroundColor, shape) 86 | ) 87 | }, 88 | interactionSource = interactionSource, 89 | value = value, 90 | onValueChange = onValueChange, 91 | editable = editable, 92 | keyboardOptions = keyboardOptions, 93 | minLines = minLines, 94 | maxLines = maxLines, 95 | singleLine = singleLine, 96 | style = overriddenStyle, 97 | overflow = overflow, 98 | contentPadding = contentPadding, 99 | placeholder = placeholderText, 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/TextInput.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.focusable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.interaction.collectIsFocusedAsState 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.IntrinsicSize 8 | import androidx.compose.foundation.layout.PaddingValues 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.foundation.layout.widthIn 12 | import androidx.compose.foundation.text.BasicTextField 13 | import androidx.compose.foundation.text.KeyboardOptions 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.derivedStateOf 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.focus.FocusRequester 23 | import androidx.compose.ui.focus.focusRequester 24 | import androidx.compose.ui.graphics.Brush 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.graphics.SolidColor 27 | import androidx.compose.ui.graphics.isSpecified 28 | import androidx.compose.ui.text.TextRange 29 | import androidx.compose.ui.text.TextStyle 30 | import androidx.compose.ui.text.font.FontFamily 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.text.input.TextFieldValue 33 | import androidx.compose.ui.text.style.TextAlign 34 | import androidx.compose.ui.text.style.TextOverflow 35 | import androidx.compose.ui.unit.TextUnit 36 | import androidx.compose.ui.unit.dp 37 | 38 | @Composable 39 | fun TextInput( 40 | value: String, 41 | onValueChange: (String) -> Unit, 42 | editable: Boolean = true, 43 | modifier: Modifier = Modifier, 44 | style: TextStyle = LocalTextStyle.current, 45 | textAlign: TextAlign = style.textAlign, 46 | fontSize: TextUnit = style.fontSize, 47 | lineHeight: TextUnit = TextUnit.Unspecified, 48 | fontWeight: FontWeight? = style.fontWeight, 49 | color: Color = style.color, 50 | fontFamily: FontFamily? = style.fontFamily, 51 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 52 | singleLine: Boolean = false, 53 | overflow: TextOverflow = TextOverflow.Ellipsis, 54 | minLines: Int = 1, 55 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 56 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 57 | contentPadding: PaddingValues = PaddingValues(0.dp), 58 | cursorBrush: Brush = SolidColor(Color.Black), 59 | placeholder: String = "", 60 | ) { 61 | val placeholderColor = color.copy(alpha = 0.4f) 62 | 63 | val currentColor = listOf( 64 | color, 65 | style.color, 66 | LocalContentColor.current 67 | ).firstOrNull { it != Color.Unspecified } ?: Color.Unspecified 68 | 69 | val currentTextAlign = listOf( 70 | textAlign, 71 | style.textAlign, 72 | ).firstOrNull { it != TextAlign.Unspecified } ?: TextAlign.Unspecified 73 | 74 | 75 | val currentLineHeight = listOf( 76 | lineHeight, 77 | style.lineHeight, 78 | ).firstOrNull { it != TextUnit.Unspecified } ?: TextUnit.Unspecified 79 | 80 | val currentStyle = style.copy( 81 | textAlign = currentTextAlign, 82 | fontSize = fontSize, 83 | color = currentColor, 84 | fontWeight = fontWeight, 85 | fontFamily = fontFamily, 86 | lineHeight = currentLineHeight 87 | ) 88 | 89 | var wasEditable by remember { mutableStateOf(editable) } 90 | 91 | var textRange by remember { mutableStateOf(TextRange(value.length, value.length)) } 92 | 93 | val isFocused by interactionSource.collectIsFocusedAsState() 94 | 95 | LaunchedEffect(editable) { 96 | if (wasEditable.not() && editable) { 97 | // just changed to editable. select all text 98 | textRange = TextRange(0, value.length) 99 | } 100 | wasEditable = editable 101 | } 102 | 103 | if (editable) { 104 | val textFieldValue by derivedStateOf { TextFieldValue(value, textRange) } 105 | val focusRequester = remember { FocusRequester() } 106 | 107 | LaunchedEffect(Unit) { 108 | if (isFocused) { 109 | focusRequester.requestFocus() 110 | } 111 | } 112 | 113 | BasicTextField( 114 | value = textFieldValue, 115 | onValueChange = { 116 | onValueChange(it.text) 117 | textRange = it.selection 118 | }, 119 | textStyle = currentStyle, 120 | singleLine = singleLine, 121 | maxLines = maxLines, 122 | minLines = minLines, 123 | modifier = modifier 124 | .focusRequester(focusRequester) 125 | .widthIn(min = 2.dp) // width for the cursor blink 126 | .width(IntrinsicSize.Min) // override default width 127 | , 128 | interactionSource = interactionSource, 129 | cursorBrush = cursorBrush, 130 | keyboardOptions = keyboardOptions 131 | ) { 132 | Box(Modifier.padding(contentPadding)) { 133 | it() 134 | if (value.isBlank()) { 135 | Text(placeholder, color = placeholderColor,style = style) 136 | } 137 | } 138 | } 139 | } else { 140 | val actualColor = if (value.isBlank() && placeholderColor.isSpecified) placeholderColor else color 141 | 142 | Text( 143 | text = value.ifBlank { placeholder }, 144 | style = currentStyle, 145 | color = actualColor, 146 | modifier = modifier 147 | .widthIn(min = 2.dp) // width for the cursor blink 148 | .focusable(interactionSource = interactionSource) 149 | .padding(contentPadding), 150 | minLines = minLines, 151 | maxLines = maxLines, 152 | overflow = overflow, 153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.LocalIndication 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.compose.runtime.staticCompositionLocalOf 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | 14 | val LocalBackgroundColor = compositionLocalOf { Color.Unspecified } 15 | val LocalOnBackgroundColor = compositionLocalOf { Color.Unspecified } 16 | val LocalPrimaryColor = compositionLocalOf { Color.Unspecified } 17 | val LocalOnPrimaryColor = compositionLocalOf { Color.Unspecified } 18 | val LocalSecondaryColor = compositionLocalOf { Color.Unspecified } 19 | val LocalOnSecondaryColor = compositionLocalOf { Color.Unspecified } 20 | val LocalSurfaceColor = compositionLocalOf { Color.Unspecified } 21 | val LocalOnSurfaceColor = compositionLocalOf { Color.Unspecified } 22 | val LocalDestructiveColor = compositionLocalOf { Color.Unspecified } 23 | val LocalOnDestructiveColor = compositionLocalOf { Color.Unspecified } 24 | val LocalOutlineColor = compositionLocalOf { Color.Unspecified } 25 | val LocalCornerRadius = compositionLocalOf { 0.dp } 26 | 27 | val LocalBodyLargeTextStyle = staticCompositionLocalOf { TextStyle.Default } 28 | val LocalBodyMediumTextStyle = staticCompositionLocalOf { TextStyle.Default } 29 | val LocalBodySmallTextStyle = staticCompositionLocalOf { TextStyle.Default } 30 | val LocalTitleLargeTextStyle = staticCompositionLocalOf { TextStyle.Default } 31 | val LocalTitleMediumTextStyle = staticCompositionLocalOf { TextStyle.Default } 32 | val LocalTitleSmallTextStyle = staticCompositionLocalOf { TextStyle.Default } 33 | 34 | val LocalTextStyle = staticCompositionLocalOf { TextStyle.Default } 35 | val LocalContentColor = compositionLocalOf { Color.Unspecified } 36 | 37 | @Composable 38 | fun Theme( 39 | onBackgroundColor: Color, 40 | backgroundColor: Color, 41 | primaryColor: Color, 42 | onPrimaryColor: Color, 43 | surfaceColor: Color, 44 | onSurfaceColor: Color, 45 | destructiveColor: Color, 46 | onDestructiveColor: Color, 47 | secondaryColor: Color, 48 | onSecondaryColor: Color, 49 | outlineColor: Color, 50 | cornerRadius: Dp, 51 | bodySmall: TextStyle, 52 | bodyMedium: TextStyle, 53 | bodyLarge: TextStyle, 54 | titleSmall: TextStyle, 55 | titleMedium: TextStyle, 56 | titleLarge: TextStyle, 57 | indication: Indication = LocalIndication.current, 58 | content: @Composable () -> Unit 59 | ) { 60 | CompositionLocalProvider( 61 | LocalTextStyle provides bodyLarge, 62 | LocalIndication provides indication, 63 | 64 | LocalOnBackgroundColor provides onBackgroundColor, 65 | LocalBackgroundColor provides backgroundColor, 66 | LocalPrimaryColor provides primaryColor, 67 | LocalOnPrimaryColor provides onPrimaryColor, 68 | LocalSurfaceColor provides surfaceColor, 69 | LocalOnSurfaceColor provides onSurfaceColor, 70 | LocalDestructiveColor provides destructiveColor, 71 | LocalOnDestructiveColor provides onDestructiveColor, 72 | LocalOutlineColor provides outlineColor, 73 | LocalSecondaryColor provides secondaryColor, 74 | LocalOnSecondaryColor provides onSecondaryColor, 75 | LocalCornerRadius provides cornerRadius, 76 | 77 | LocalBodySmallTextStyle provides bodySmall, 78 | LocalBodyMediumTextStyle provides bodyMedium, 79 | LocalBodyLargeTextStyle provides bodyLarge, 80 | 81 | LocalTitleLargeTextStyle provides titleLarge, 82 | LocalTitleMediumTextStyle provides titleMedium, 83 | LocalTitleSmallTextStyle provides titleSmall, 84 | 85 | LocalTextStyle provides bodyMedium, 86 | ) { 87 | content() 88 | } 89 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/Toggle.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.animateDpAsState 5 | import androidx.compose.foundation.LocalIndication 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.PaddingValues 10 | import androidx.compose.foundation.layout.calculateEndPadding 11 | import androidx.compose.foundation.layout.calculateStartPadding 12 | import androidx.compose.foundation.layout.offset 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.widthIn 16 | import androidx.compose.foundation.selection.toggleable 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.derivedStateOf 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.clip 26 | import androidx.compose.ui.draw.shadow 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.Shape 29 | import androidx.compose.ui.layout.onPlaced 30 | import androidx.compose.ui.platform.LocalDensity 31 | import androidx.compose.ui.platform.LocalLayoutDirection 32 | import androidx.compose.ui.semantics.Role 33 | import androidx.compose.ui.unit.dp 34 | 35 | @Composable 36 | fun Toggle( 37 | toggled: Boolean, 38 | modifier: Modifier = Modifier, 39 | onToggled: ((Boolean) -> Unit)? = null, 40 | enabled: Boolean = true, 41 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 42 | toggledColor: Color = LocalPrimaryColor.current, 43 | backgroundColor: Color = LocalContentColor.current.copy(0.44f), 44 | contentPadding: PaddingValues = PaddingValues(0.dp), 45 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 46 | thumb: @Composable () -> Unit, 47 | ) { 48 | var trackWidth by remember { mutableStateOf(0.dp) } 49 | 50 | val layoutDirection = LocalLayoutDirection.current 51 | 52 | val paddingEnd by derivedStateOf { 53 | contentPadding.calculateEndPadding(layoutDirection) + contentPadding.calculateStartPadding(layoutDirection) 54 | } 55 | 56 | val maxOffset by derivedStateOf { (trackWidth / 2) - paddingEnd } 57 | val offset by animateDpAsState(if (toggled) maxOffset else 0.dp) 58 | 59 | val backgroundColor by animateColorAsState(if (toggled) toggledColor else backgroundColor) 60 | 61 | val density = LocalDensity.current 62 | 63 | Box( 64 | modifier = modifier 65 | .widthIn(min = 48.dp) 66 | .clip(shape) 67 | .background(backgroundColor, shape) 68 | .onPlaced { trackWidth = with(density) { it.size.width.toDp() } } 69 | then (if (onToggled != null) 70 | Modifier.toggleable( 71 | value = toggled, 72 | enabled = enabled, 73 | interactionSource = interactionSource, 74 | indication = LocalIndication.current, 75 | role = Role.Switch 76 | ) { onToggled(toggled.not()) } 77 | else Modifier) 78 | .padding(contentPadding) 79 | ) { 80 | Box(Modifier.offset(x = offset)) { 81 | thumb() 82 | } 83 | } 84 | } 85 | 86 | 87 | @Composable 88 | fun Thumb( 89 | modifier: Modifier = Modifier, 90 | shape: Shape = RoundedCornerShape(LocalCornerRadius.current), 91 | color: Color = LocalSurfaceColor.current 92 | ) { 93 | Box( 94 | modifier 95 | .shadow(2.dp, shape) 96 | .clip(shape) 97 | .background(color) 98 | .size(24.dp) 99 | ) 100 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/builtwithpaper/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxHeight 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.shadow 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun TopAppBar( 23 | modifier: Modifier = Modifier, 24 | navigation: (@Composable () -> Unit)? = null, 25 | title: @Composable () -> Unit = {}, 26 | actions: @Composable () -> Unit = {}, 27 | elevation: Dp = 0.dp, 28 | contentColor: Color = LocalOnSurfaceColor.current, 29 | backgroundColor: Color = LocalSurfaceColor.current, 30 | contentPadding: PaddingValues = PaddingValues(0.dp), 31 | ) { 32 | Row( 33 | modifier = modifier 34 | .shadow(elevation) 35 | .background(backgroundColor) 36 | .padding(contentPadding), 37 | verticalAlignment = Alignment.CenterVertically, 38 | horizontalArrangement = Arrangement.spacedBy(8.dp), 39 | ) { 40 | CompositionLocalProvider(LocalContentColor provides contentColor) { 41 | if (navigation == null) { 42 | Spacer(Modifier.width(12.dp).fillMaxHeight()) 43 | } else { 44 | Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.CenterStart) { 45 | navigation() 46 | } 47 | } 48 | Box( 49 | modifier = Modifier.weight(1f).fillMaxHeight(), 50 | contentAlignment = Alignment.CenterStart 51 | ) { 52 | title() 53 | } 54 | Row( 55 | verticalAlignment = Alignment.CenterVertically, 56 | horizontalArrangement = Arrangement.spacedBy(8.dp), 57 | modifier = Modifier.fillMaxHeight() 58 | ) { 59 | actions() 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/AddContact.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalSurfaceColor 10 | import androidx.compose.foundation.verticalScroll 11 | import androidx.compose.foundation.rememberScrollState 12 | import com.builtwithpaper.LocalOnSurfaceColor 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import com.builtwithpaper.LocalContentColor 15 | import com.builtwithpaper.TopAppBar 16 | import androidx.compose.foundation.layout.height 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.foundation.layout.fillMaxWidth 19 | import com.builtwithpaper.GhostButton 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import com.composables.icons.lucide.LucideIcon 22 | import com.builtwithpaper.Text 23 | import androidx.compose.ui.unit.sp 24 | import androidx.compose.foundation.layout.Arrangement 25 | import androidx.compose.foundation.layout.widthIn 26 | import androidx.compose.foundation.layout.fillMaxHeight 27 | import androidx.compose.foundation.layout.padding 28 | import com.builtwithpaper.SecondaryButton 29 | import com.builtwithpaper.LocalOnSecondaryColor 30 | import com.builtwithpaper.LocalSecondaryColor 31 | import androidx.compose.foundation.layout.Row 32 | import com.builtwithpaper.LocalPrimaryColor 33 | import com.builtwithpaper.TextField 34 | import androidx.compose.foundation.shape.RoundedCornerShape 35 | import com.builtwithpaper.LocalCornerRadius 36 | import androidx.compose.ui.text.TextStyle 37 | import androidx.compose.ui.draw.alpha 38 | 39 | @Composable 40 | fun AddContact() { 41 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalSurfaceColor.current).verticalScroll(rememberScrollState())) { 42 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 43 | TopAppBar( 44 | modifier = Modifier.height(56.dp).fillMaxWidth(), 45 | navigation = { 46 | GhostButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 47 | LucideIcon("arrow-left", contentDescription = "", tint = LocalContentColor.current) 48 | } 49 | }, 50 | title = { 51 | Text("Add Contact", fontSize = 16.sp, maxLines = 99) 52 | }, 53 | contentPadding = PaddingValues(4.dp) 54 | ) 55 | Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().verticalScroll(rememberScrollState()).padding(16.dp)) { 56 | SecondaryButton(onClick = { /* TODO Handle this */ }, contentColor = LocalOnSecondaryColor.current, backgroundColor = LocalSecondaryColor.current, contentPadding = PaddingValues(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), borderColor = LocalSecondaryColor.current, modifier = Modifier.height(200.dp).fillMaxWidth()) { 57 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize()) { 58 | LucideIcon("image-plus", contentDescription = "", tint = LocalContentColor.current) 59 | } 60 | } 61 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 62 | LucideIcon("contact-round", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 63 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "First name", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 64 | } 65 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 66 | LucideIcon("contact-round", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.alpha(0f).padding(16.dp)) 67 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Last name", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 68 | } 69 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 70 | LucideIcon("phone", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 71 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Phone number", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 72 | } 73 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 74 | LucideIcon("map-pin", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 75 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Address", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 76 | } 77 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 78 | LucideIcon("contact-round", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.alpha(0f).padding(16.dp)) 79 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "City", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 80 | } 81 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 82 | LucideIcon("contact-round", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.alpha(0f).padding(16.dp)) 83 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Post code", contentPadding = PaddingValues(16.dp), borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/App.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.compose.rememberNavController 5 | import com.builtwithpaper.Theme 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.text.TextStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.unit.sp 10 | import androidx.compose.ui.unit.dp 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | 14 | @Composable 15 | fun App() { 16 | val navController = rememberNavController() 17 | Theme(primaryColor = Color(0xFF2563EB), onPrimaryColor = Color(0xFFFFFFFF), secondaryColor = Color(0xFFE0F2FE), onSecondaryColor = Color(0xFF1A1A1A), backgroundColor = Color(0xFFFAFAFA), onBackgroundColor = Color(0xFF0F172A), surfaceColor = Color(0xFFFFFFFF), onSurfaceColor = Color(0xFF1A1A1A), destructiveColor = Color(0xFFDC2626), onDestructiveColor = Color(0xFFFFFFFF), outlineColor = Color(0xFFD1D5DB), titleLarge = TextStyle(fontWeight = FontWeight(600), fontSize = 32.sp, lineHeight = 27.sp), titleMedium = TextStyle(fontWeight = FontWeight(500), fontSize = 24.sp, lineHeight = 18.sp), titleSmall = TextStyle(fontWeight = FontWeight(500), fontSize = 18.sp, lineHeight = 15.sp), bodyLarge = TextStyle(fontWeight = FontWeight(400), fontSize = 16.sp, lineHeight = 21.sp), bodyMedium = TextStyle(fontWeight = FontWeight(400), fontSize = 16.sp, lineHeight = 18.sp), bodySmall = TextStyle(fontWeight = FontWeight(400), fontSize = 10.sp, lineHeight = 15.sp), cornerRadius = 24.dp) { 18 | NavHost(navController = navController, startDestination = "Conversations") { 19 | composable(route = "Conversations") { 20 | Conversations() 21 | } 22 | composable(route = "Conversation") { 23 | Conversation() 24 | } 25 | composable(route = "Profile") { 26 | Profile() 27 | } 28 | composable(route = "Search") { 29 | Search() 30 | } 31 | composable(route = "Contacts") { 32 | Contacts() 33 | } 34 | composable(route = "Settings") { 35 | Settings() 36 | } 37 | composable(route = "AddContact") { 38 | AddContact() 39 | } 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Contacts.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalBackgroundColor 10 | import com.builtwithpaper.LocalOnBackgroundColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import androidx.compose.foundation.layout.widthIn 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.fillMaxHeight 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.layout.Row 19 | import androidx.compose.foundation.layout.Arrangement 20 | import androidx.compose.foundation.layout.height 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import androidx.compose.ui.draw.shadow 24 | import androidx.compose.ui.zIndex 25 | import com.builtwithpaper.LocalSurfaceColor 26 | import com.builtwithpaper.LocalOnSurfaceColor 27 | import com.composables.icons.lucide.LucideIcon 28 | import com.builtwithpaper.TextField 29 | import androidx.compose.foundation.layout.PaddingValues 30 | import androidx.compose.ui.text.TextStyle 31 | import androidx.compose.ui.unit.sp 32 | import com.builtwithpaper.GhostButton 33 | import androidx.compose.foundation.lazy.LazyColumn 34 | import com.builtwithpaper.Text 35 | import androidx.compose.ui.text.font.FontWeight 36 | import com.builtwithpaper.LocalPrimaryColor 37 | import androidx.compose.foundation.Image 38 | import coil3.compose.rememberAsyncImagePainter 39 | import androidx.compose.foundation.layout.width 40 | import androidx.compose.ui.layout.ContentScale 41 | import androidx.compose.ui.draw.alpha 42 | import com.builtwithpaper.BottomNavigation 43 | import androidx.compose.foundation.layout.heightIn 44 | import androidx.compose.ui.graphics.RectangleShape 45 | import androidx.compose.ui.graphics.Color 46 | import com.builtwithpaper.currentScreenWidthBreakpoint 47 | import com.builtwithpaper.isAtLeast 48 | import com.builtwithpaper.ScreenWidthBreakpoint 49 | import com.builtwithpaper.TabItem 50 | 51 | @Composable 52 | fun Contacts() { 53 | val breakpoint = currentScreenWidthBreakpoint() 54 | 55 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalBackgroundColor.current)) { 56 | val breakpoint = currentScreenWidthBreakpoint() 57 | 58 | CompositionLocalProvider(LocalContentColor provides LocalOnBackgroundColor.current) { 59 | Column(modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().padding(top = 16.dp, end = 0.dp, bottom = 0.dp, start = 0.dp)) { 60 | Row(modifier = Modifier.padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 61 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.height(56.dp).weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(100.dp)).shadow(4.dp, RoundedCornerShape(100.dp)).zIndex(4f).background(LocalSurfaceColor.current).padding(top = 0.dp, end = 8.dp, bottom = 0.dp, start = 16.dp)) { 62 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 63 | LucideIcon("search", contentDescription = "", tint = LocalContentColor.current) 64 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Search", contentPadding = PaddingValues(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), textStyle = TextStyle.Default, fontSize = 16.sp) 65 | Row { 66 | GhostButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 12.dp)) { 67 | LucideIcon("circle-plus", contentDescription = "", tint = LocalContentColor.current) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | LazyColumn(contentPadding = PaddingValues(start = 0.dp, top = 16.dp, end = 0.dp, bottom = 16.dp), modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight()) { 74 | item { 75 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 76 | Text("A", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 77 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1607746882042-944635dfe10e?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 78 | Text("Alicia", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 79 | } 80 | } 81 | item { 82 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 83 | Text("A", modifier = Modifier.alpha(0f).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 84 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1650630729397-810915b32ac9?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 85 | Text("Antony", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 86 | } 87 | } 88 | item { 89 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 90 | Text("B", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 91 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 92 | Text("Ben", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 93 | } 94 | } 95 | item { 96 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 97 | Text("B", modifier = Modifier.alpha(0f).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 98 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1508186225823-0963cf9ab0de?q=80&w=3166&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 99 | Text("Brianna", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 100 | } 101 | } 102 | item { 103 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 104 | Text("C", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 105 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 106 | Text("Cindy", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 107 | } 108 | } 109 | item { 110 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 111 | Text("D", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 112 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=3276&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 113 | Text("Daisy", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 114 | } 115 | } 116 | item { 117 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 118 | Text("N", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 119 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 120 | Text("Naomi", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 121 | } 122 | } 123 | item { 124 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 125 | Text("P", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 126 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 127 | Text("Paolo", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 128 | } 129 | } 130 | item { 131 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 132 | Text("P", modifier = Modifier.alpha(0f).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 133 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1599566147214-ce487862ea4f?q=80&w=3347&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 134 | Text("Peter", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 135 | } 136 | } 137 | item { 138 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 139 | Text("P", modifier = Modifier.alpha(0f).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 16.dp), fontSize = 16.sp, fontWeight = FontWeight(700), color = LocalPrimaryColor.current, maxLines = 99) 140 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 141 | Text("Petra", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 142 | } 143 | } 144 | } 145 | } 146 | BottomNavigation(modifier = Modifier.heightIn(56.dp).fillMaxWidth().shadow(4.dp, RectangleShape).zIndex(4f).background(if (breakpoint isAtLeast ScreenWidthBreakpoint.Small) LocalSurfaceColor.current else Color.Unspecified), contentColor = LocalOnSurfaceColor.current, backgroundColor = LocalSurfaceColor.current) { 147 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 148 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 149 | LucideIcon("house", contentDescription = "", tint = LocalContentColor.current) 150 | Text("Chats", fontSize = 16.sp, maxLines = 99) 151 | } 152 | } 153 | TabItem(selected = true, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 154 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 155 | LucideIcon("contact", contentDescription = "", tint = LocalContentColor.current) 156 | Text("Contacts", fontSize = 16.sp, maxLines = 99) 157 | } 158 | } 159 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 160 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 161 | LucideIcon("settings", contentDescription = "", tint = LocalContentColor.current) 162 | Text("Settings", fontSize = 16.sp, maxLines = 99) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Conversation.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalBackgroundColor 10 | import com.builtwithpaper.LocalOnBackgroundColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import com.builtwithpaper.TopAppBar 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import com.builtwithpaper.GhostButton 18 | import androidx.compose.foundation.layout.PaddingValues 19 | import com.composables.icons.lucide.LucideIcon 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import com.builtwithpaper.LocalCornerRadius 24 | import com.builtwithpaper.Text 25 | import androidx.compose.ui.unit.sp 26 | import androidx.compose.foundation.lazy.LazyColumn 27 | import androidx.compose.foundation.layout.widthIn 28 | import androidx.compose.foundation.layout.fillMaxHeight 29 | import androidx.compose.foundation.layout.Row 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.layout.Box 32 | import androidx.compose.foundation.layout.width 33 | import androidx.compose.foundation.Image 34 | import coil3.compose.rememberAsyncImagePainter 35 | import androidx.compose.ui.layout.ContentScale 36 | import com.builtwithpaper.LocalSurfaceColor 37 | import com.builtwithpaper.LocalOnSurfaceColor 38 | import com.builtwithpaper.LocalPrimaryColor 39 | import com.builtwithpaper.LocalOnPrimaryColor 40 | import androidx.compose.ui.draw.alpha 41 | import com.builtwithpaper.Card 42 | import com.builtwithpaper.TextField 43 | import androidx.compose.ui.text.TextStyle 44 | 45 | @Composable 46 | fun Conversation() { 47 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalBackgroundColor.current)) { 48 | CompositionLocalProvider(LocalContentColor provides LocalOnBackgroundColor.current) { 49 | TopAppBar( 50 | modifier = Modifier.height(56.dp).fillMaxWidth(), 51 | navigation = { 52 | GhostButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 53 | LucideIcon("arrow-left", contentDescription = "", tint = LocalContentColor.current) 54 | } 55 | }, 56 | title = { 57 | Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(LocalCornerRadius.current))) { 58 | Text("Kim", fontSize = 16.sp, maxLines = 99) 59 | } 60 | }, 61 | backgroundColor = LocalBackgroundColor.current, 62 | contentPadding = PaddingValues(4.dp) 63 | ) 64 | LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(start = 0.dp, top = 16.dp, end = 0.dp, bottom = 16.dp), modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight()) { 65 | item { 66 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 67 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(44.dp).height(44.dp).clip(RoundedCornerShape(100.dp))) { 68 | Text("K", fontSize = 16.sp, maxLines = 99) 69 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "Kim's Photo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 70 | } 71 | Column(modifier = Modifier.clip(RoundedCornerShape(LocalCornerRadius.current)).background(LocalSurfaceColor.current).padding(top = 14.dp, end = 16.dp, bottom = 14.dp, start = 16.dp)) { 72 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 73 | Text("Looking forward to the trip.", fontSize = 16.sp, maxLines = 99) 74 | } 75 | } 76 | } 77 | } 78 | item { 79 | Row(horizontalArrangement = Arrangement.spacedBy(space = 8.dp, alignment = Alignment.End), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 80 | Column(modifier = Modifier.clip(RoundedCornerShape(LocalCornerRadius.current)).background(LocalPrimaryColor.current).padding(top = 14.dp, end = 16.dp, bottom = 14.dp, start = 16.dp)) { 81 | CompositionLocalProvider(LocalContentColor provides LocalOnPrimaryColor.current) { 82 | Text("Same here! Can't wait", fontSize = 16.sp, maxLines = 99) 83 | } 84 | } 85 | } 86 | } 87 | item { 88 | Row(horizontalArrangement = Arrangement.spacedBy(space = 8.dp, alignment = Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 89 | Text("Today 17:30", modifier = Modifier.alpha(0.66f), fontSize = 12.sp, maxLines = 99) 90 | } 91 | } 92 | item { 93 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Bottom, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 94 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(44.dp).height(44.dp).clip(RoundedCornerShape(100.dp))) { 95 | Text("K", fontSize = 16.sp, maxLines = 99) 96 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "Kim's Photo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 97 | } 98 | Card(elevation = 0.dp, backgroundColor = LocalSurfaceColor.current, contentColor = LocalOnSurfaceColor.current, shape = RoundedCornerShape(LocalCornerRadius.current)) { 99 | Column { 100 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "Hero Image", modifier = Modifier.height(200.dp).fillMaxWidth(), contentScale = ContentScale.Crop) 101 | Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxWidth().padding(16.dp)) { 102 | Text("Fishing by the sea side", modifier = Modifier.fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 103 | Text("Find your next dream destination", modifier = Modifier.fillMaxWidth().alpha(0.6f), maxLines = 99) 104 | } 105 | } 106 | } 107 | } 108 | } 109 | item { 110 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Bottom, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 111 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(44.dp).height(44.dp).clip(RoundedCornerShape(100.dp))) { 112 | Text("K", fontSize = 16.sp, maxLines = 99) 113 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "Kim's Photo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 114 | } 115 | Column(modifier = Modifier.clip(RoundedCornerShape(LocalCornerRadius.current)).background(LocalSurfaceColor.current).padding(top = 14.dp, end = 16.dp, bottom = 14.dp, start = 16.dp)) { 116 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 117 | Text("What do you think?", fontSize = 16.sp, maxLines = 99) 118 | } 119 | } 120 | } 121 | } 122 | item { 123 | Row(horizontalArrangement = Arrangement.spacedBy(space = 8.dp, alignment = Alignment.End), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 0.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 124 | Column(modifier = Modifier.clip(RoundedCornerShape(LocalCornerRadius.current)).background(LocalPrimaryColor.current).padding(top = 14.dp, end = 16.dp, bottom = 14.dp, start = 16.dp)) { 125 | CompositionLocalProvider(LocalContentColor provides LocalOnPrimaryColor.current) { 126 | Text("It's perfect!", fontSize = 16.sp, maxLines = 99) 127 | } 128 | } 129 | } 130 | } 131 | } 132 | Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp)) { 133 | GhostButton(onClick = { /* TODO Handle this */ }, contentColor = LocalPrimaryColor.current, contentPadding = PaddingValues(12.dp)) { 134 | LucideIcon("camera", contentDescription = "", tint = LocalContentColor.current) 135 | } 136 | GhostButton(onClick = { /* TODO Handle this */ }, contentColor = LocalPrimaryColor.current, contentPadding = PaddingValues(12.dp)) { 137 | LucideIcon("image-play", contentDescription = "", tint = LocalContentColor.current) 138 | } 139 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 5, placeholderText = "Type your message", contentPadding = PaddingValues(16.dp), contentColor = LocalOnSurfaceColor.current, backgroundColor = LocalSurfaceColor.current, borderWidth = 1.dp, shape = RoundedCornerShape(LocalCornerRadius.current), textStyle = TextStyle.Default, fontSize = 16.sp) 140 | } 141 | } 142 | } 143 | } 144 | 145 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Conversations.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalBackgroundColor 10 | import com.builtwithpaper.LocalOnBackgroundColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import androidx.compose.foundation.layout.Arrangement 14 | import androidx.compose.foundation.layout.widthIn 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import androidx.compose.foundation.layout.fillMaxHeight 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.foundation.layout.Row 20 | import androidx.compose.foundation.layout.height 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import androidx.compose.ui.draw.shadow 24 | import androidx.compose.ui.zIndex 25 | import com.builtwithpaper.LocalSurfaceColor 26 | import com.builtwithpaper.LocalOnSurfaceColor 27 | import com.composables.icons.lucide.LucideIcon 28 | import com.builtwithpaper.Text 29 | import androidx.compose.ui.draw.alpha 30 | import androidx.compose.ui.unit.sp 31 | import com.builtwithpaper.GhostButton 32 | import androidx.compose.foundation.layout.PaddingValues 33 | import androidx.compose.foundation.lazy.LazyColumn 34 | import com.builtwithpaper.LocalCornerRadius 35 | import androidx.compose.foundation.Image 36 | import coil3.compose.rememberAsyncImagePainter 37 | import androidx.compose.foundation.layout.width 38 | import androidx.compose.ui.layout.ContentScale 39 | import com.builtwithpaper.LocalPrimaryColor 40 | import com.builtwithpaper.LocalOnPrimaryColor 41 | import androidx.compose.ui.text.style.TextAlign 42 | import androidx.compose.ui.text.font.FontWeight 43 | import com.builtwithpaper.BottomNavigation 44 | import androidx.compose.foundation.layout.heightIn 45 | import androidx.compose.ui.graphics.RectangleShape 46 | import androidx.compose.ui.graphics.Color 47 | import com.builtwithpaper.currentScreenWidthBreakpoint 48 | import com.builtwithpaper.isAtLeast 49 | import com.builtwithpaper.ScreenWidthBreakpoint 50 | import com.builtwithpaper.TabItem 51 | 52 | @Composable 53 | fun Conversations() { 54 | val breakpoint = currentScreenWidthBreakpoint() 55 | 56 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalBackgroundColor.current)) { 57 | val breakpoint = currentScreenWidthBreakpoint() 58 | 59 | CompositionLocalProvider(LocalContentColor provides LocalOnBackgroundColor.current) { 60 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().padding(top = 16.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 61 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.height(56.dp).fillMaxWidth().clip(RoundedCornerShape(100.dp)).shadow(4.dp, RoundedCornerShape(100.dp)).zIndex(4f).background(LocalSurfaceColor.current).padding(top = 0.dp, end = 8.dp, bottom = 0.dp, start = 16.dp)) { 62 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 63 | LucideIcon("search", contentDescription = "", tint = LocalContentColor.current) 64 | Text("Search", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.4f).padding(top = 12.dp, end = 8.dp, bottom = 12.dp, start = 8.dp), fontSize = 16.sp, maxLines = 99) 65 | Row { 66 | GhostButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 67 | LucideIcon("message-circle-plus", contentDescription = "", tint = LocalContentColor.current) 68 | } 69 | } 70 | } 71 | } 72 | LazyColumn(contentPadding = PaddingValues(start = 0.dp, top = 16.dp, end = 0.dp, bottom = 16.dp), modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight()) { 73 | item { 74 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 75 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "Jenny's profile photo", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 76 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 77 | Row { 78 | Text("Kim", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 79 | Text("3min", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 80 | } 81 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 82 | Text("What do you think?", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 83 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.width(22.dp).height(22.dp).clip(RoundedCornerShape(100.dp)).background(LocalPrimaryColor.current)) { 84 | CompositionLocalProvider(LocalContentColor provides LocalOnPrimaryColor.current) { 85 | Text("1", modifier = Modifier.clip(RoundedCornerShape(100.dp)), textAlign = TextAlign.Center, fontSize = 12.sp, fontWeight = FontWeight(500), maxLines = 1) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | item { 93 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 94 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1599566147214-ce487862ea4f?q=80&w=3347&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 95 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 96 | Row { 97 | Text("Peter", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 98 | Text("15min", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 99 | } 100 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 101 | Text("Isn't that swell?", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 102 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.width(22.dp).height(22.dp).clip(RoundedCornerShape(100.dp)).background(LocalPrimaryColor.current)) { 103 | CompositionLocalProvider(LocalContentColor provides LocalOnPrimaryColor.current) { 104 | Text("2", modifier = Modifier.clip(RoundedCornerShape(100.dp)), textAlign = TextAlign.Center, fontSize = 12.sp, fontWeight = FontWeight(500), maxLines = 1) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | item { 112 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 113 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 114 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 115 | Row { 116 | Text("Naomi", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 117 | Text("Fri", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 118 | } 119 | Row { 120 | Text("Had a great time too! 😍", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 121 | } 122 | } 123 | } 124 | } 125 | item { 126 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 127 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 128 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 129 | Row { 130 | Text("Petra", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 131 | Text("Wed", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 132 | } 133 | Row { 134 | Text("What do you think?", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 135 | } 136 | } 137 | } 138 | } 139 | item { 140 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 141 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 142 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 143 | Row { 144 | Text("Paolo", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 145 | Text("Wed", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 146 | } 147 | Row { 148 | Text("Paolo sent a photo.", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 149 | } 150 | } 151 | } 152 | } 153 | item { 154 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(8.dp)) { 155 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1607746882042-944635dfe10e?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "Jenny's profile photo", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 156 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { 157 | Row { 158 | Text("Alicia", modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 159 | Text("Mon", modifier = Modifier.alpha(0.6f), fontSize = 16.sp, maxLines = 99) 160 | } 161 | Row { 162 | Text("Is that what really happened?", modifier = Modifier.weight(1f, fill = false).fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 1) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | BottomNavigation(modifier = Modifier.heightIn(56.dp).fillMaxWidth().shadow(4.dp, RectangleShape).zIndex(4f).background(if (breakpoint isAtLeast ScreenWidthBreakpoint.Small) LocalSurfaceColor.current else Color.Unspecified), contentColor = LocalOnSurfaceColor.current, backgroundColor = LocalSurfaceColor.current) { 170 | TabItem(selected = true, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 171 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 172 | LucideIcon("house", contentDescription = "", tint = LocalContentColor.current) 173 | Text("Chats", fontSize = 16.sp, maxLines = 99) 174 | } 175 | } 176 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 177 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 178 | LucideIcon("contact", contentDescription = "", tint = LocalContentColor.current) 179 | Text("Contacts", fontSize = 16.sp, maxLines = 99) 180 | } 181 | } 182 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 183 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 184 | LucideIcon("settings", contentDescription = "", tint = LocalContentColor.current) 185 | Text("Settings", fontSize = 16.sp, maxLines = 99) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Profile.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalSurfaceColor 10 | import com.builtwithpaper.LocalOnSurfaceColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import com.builtwithpaper.TopAppBar 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import com.builtwithpaper.GhostButton 18 | import androidx.compose.foundation.layout.PaddingValues 19 | import com.composables.icons.lucide.LucideIcon 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.foundation.layout.widthIn 22 | import androidx.compose.foundation.layout.fillMaxHeight 23 | import androidx.compose.foundation.verticalScroll 24 | import androidx.compose.foundation.rememberScrollState 25 | import androidx.compose.foundation.layout.Arrangement 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.foundation.Image 28 | import coil3.compose.rememberAsyncImagePainter 29 | import androidx.compose.foundation.layout.width 30 | import androidx.compose.ui.draw.clip 31 | import androidx.compose.foundation.shape.RoundedCornerShape 32 | import androidx.compose.ui.layout.ContentScale 33 | import com.builtwithpaper.Text 34 | import androidx.compose.ui.unit.sp 35 | import androidx.compose.ui.text.font.FontWeight 36 | import com.builtwithpaper.LocalPrimaryColor 37 | import androidx.compose.ui.draw.alpha 38 | import androidx.compose.foundation.layout.Row 39 | import com.builtwithpaper.PrimaryButton 40 | 41 | @Composable 42 | fun Profile() { 43 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalSurfaceColor.current)) { 44 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 45 | TopAppBar( 46 | modifier = Modifier.height(56.dp).fillMaxWidth(), 47 | navigation = { 48 | GhostButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 49 | LucideIcon("arrow-left", contentDescription = "", tint = LocalContentColor.current) 50 | } 51 | }, 52 | contentColor = LocalContentColor.current, 53 | backgroundColor = Color.Unspecified, 54 | contentPadding = PaddingValues(4.dp) 55 | ) 56 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().verticalScroll(rememberScrollState())) { 57 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().padding(top = 8.dp, end = 0.dp, bottom = 8.dp, start = 0.dp)) { 58 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "Kim's Photo", modifier = Modifier.width(120.dp).height(120.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 59 | Text("Kim", fontSize = 32.sp, fontWeight = FontWeight(500), color = LocalPrimaryColor.current, maxLines = 99) 60 | Text("+44 7519 654123", modifier = Modifier.alpha(0.66f).clip(RoundedCornerShape(4.dp)), fontSize = 16.sp, maxLines = 99) 61 | } 62 | Row { 63 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f, fill = false).fillMaxWidth().padding(16.dp)) { 64 | PrimaryButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 65 | LucideIcon("send", contentDescription = "", tint = LocalContentColor.current) 66 | } 67 | Text("Message", fontSize = 16.sp, color = LocalPrimaryColor.current, maxLines = 99) 68 | } 69 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f, fill = false).fillMaxWidth().padding(16.dp)) { 70 | PrimaryButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp)) { 71 | LucideIcon("phone", contentDescription = "", tint = LocalContentColor.current) 72 | } 73 | Text("Message", fontSize = 16.sp, color = LocalPrimaryColor.current, maxLines = 99) 74 | } 75 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f, fill = false).fillMaxWidth().padding(16.dp)) { 76 | PrimaryButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(8.dp), borderWidth = 0.dp) { 77 | LucideIcon("video", contentDescription = "", tint = LocalContentColor.current) 78 | } 79 | Text("Video", fontSize = 16.sp, color = LocalPrimaryColor.current, maxLines = 99) 80 | } 81 | } 82 | Column(modifier = Modifier.fillMaxWidth()) { 83 | Text("More actions", modifier = Modifier.padding(16.dp), fontSize = 16.sp, fontWeight = FontWeight(500), color = LocalPrimaryColor.current, maxLines = 99) 84 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 85 | LucideIcon("file-video", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 86 | Text("View Media", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 87 | } 88 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 89 | LucideIcon("search", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 90 | Text("Search in conversation", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 91 | } 92 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 93 | LucideIcon("bell", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 94 | Text("Notifications", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Search.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalBackgroundColor 10 | import com.builtwithpaper.LocalOnBackgroundColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import androidx.compose.foundation.layout.Arrangement 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.foundation.layout.widthIn 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.Row 18 | import androidx.compose.foundation.layout.height 19 | import androidx.compose.foundation.layout.fillMaxWidth 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.ui.draw.shadow 23 | import androidx.compose.ui.zIndex 24 | import com.builtwithpaper.LocalSurfaceColor 25 | import com.builtwithpaper.LocalOnSurfaceColor 26 | import com.composables.icons.lucide.LucideIcon 27 | import com.builtwithpaper.TextField 28 | import androidx.compose.foundation.layout.PaddingValues 29 | import androidx.compose.ui.text.TextStyle 30 | import androidx.compose.ui.unit.sp 31 | import androidx.compose.foundation.horizontalScroll 32 | import androidx.compose.foundation.rememberScrollState 33 | import androidx.compose.foundation.layout.Box 34 | import androidx.compose.foundation.layout.width 35 | import com.builtwithpaper.Text 36 | import androidx.compose.foundation.Image 37 | import coil3.compose.rememberAsyncImagePainter 38 | import androidx.compose.ui.layout.ContentScale 39 | import com.builtwithpaper.LocalPrimaryColor 40 | 41 | @Composable 42 | fun Search() { 43 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalBackgroundColor.current)) { 44 | CompositionLocalProvider(LocalContentColor provides LocalOnBackgroundColor.current) { 45 | Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.widthIn(max = 600.dp).padding(16.dp)) { 46 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.height(56.dp).fillMaxWidth().clip(RoundedCornerShape(100.dp)).shadow(4.dp, RoundedCornerShape(100.dp)).zIndex(4f).background(LocalSurfaceColor.current).padding(top = 0.dp, end = 8.dp, bottom = 0.dp, start = 16.dp)) { 47 | CompositionLocalProvider(LocalContentColor provides LocalOnSurfaceColor.current) { 48 | LucideIcon("search", contentDescription = "", tint = LocalContentColor.current) 49 | TextField(value = "", onValueChange = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth(), maxLines = 1, placeholderText = "Search...", contentPadding = PaddingValues(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), textStyle = TextStyle.Default, fontSize = 16.sp) 50 | Row { 51 | } 52 | } 53 | } 54 | Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState())) { 55 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { 56 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp))) { 57 | Text("K", fontSize = 16.sp, maxLines = 99) 58 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?q=80&w=3534&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 59 | } 60 | Text("Jenny", fontSize = 16.sp, maxLines = 99) 61 | } 62 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { 63 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp))) { 64 | Text("K", fontSize = 16.sp, maxLines = 99) 65 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1599566147214-ce487862ea4f?q=80&w=3347&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 66 | } 67 | Text("Peter", fontSize = 16.sp, maxLines = 99) 68 | } 69 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { 70 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp))) { 71 | Text("K", fontSize = 16.sp, maxLines = 99) 72 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80"), contentDescription = "", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 73 | } 74 | Text("Naomi", fontSize = 16.sp, maxLines = 99) 75 | } 76 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) { 77 | Box(contentAlignment = Alignment.Center, modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp))) { 78 | Text("K", fontSize = 16.sp, maxLines = 99) 79 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 80 | } 81 | Text("Petra", fontSize = 16.sp, maxLines = 99) 82 | } 83 | } 84 | Column(modifier = Modifier.fillMaxWidth()) { 85 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 86 | LucideIcon("file-image", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 87 | Text("Photos", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 88 | } 89 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 90 | LucideIcon("file-video", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 91 | Text("Videos", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 92 | } 93 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 94 | LucideIcon("headphones", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 95 | Text("Music", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 96 | } 97 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 98 | LucideIcon("globe", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 99 | Text("Links", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/example/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.background 9 | import com.builtwithpaper.LocalBackgroundColor 10 | import com.builtwithpaper.LocalOnBackgroundColor 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import com.builtwithpaper.LocalContentColor 13 | import com.builtwithpaper.TopAppBar 14 | import androidx.compose.foundation.layout.height 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import com.builtwithpaper.Text 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.ui.unit.sp 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.foundation.layout.PaddingValues 23 | import androidx.compose.foundation.layout.Arrangement 24 | import androidx.compose.foundation.layout.widthIn 25 | import androidx.compose.foundation.layout.fillMaxHeight 26 | import androidx.compose.foundation.verticalScroll 27 | import androidx.compose.foundation.rememberScrollState 28 | import androidx.compose.foundation.layout.Row 29 | import androidx.compose.ui.draw.clip 30 | import androidx.compose.foundation.shape.RoundedCornerShape 31 | import com.builtwithpaper.LocalCornerRadius 32 | import androidx.compose.foundation.Image 33 | import coil3.compose.rememberAsyncImagePainter 34 | import androidx.compose.foundation.layout.width 35 | import androidx.compose.ui.layout.ContentScale 36 | import androidx.compose.ui.draw.alpha 37 | import com.builtwithpaper.PrimaryButton 38 | import com.builtwithpaper.LocalPrimaryColor 39 | import com.composables.icons.lucide.LucideIcon 40 | import com.builtwithpaper.BottomNavigation 41 | import androidx.compose.foundation.layout.heightIn 42 | import androidx.compose.ui.draw.shadow 43 | import androidx.compose.ui.graphics.RectangleShape 44 | import androidx.compose.ui.zIndex 45 | import com.builtwithpaper.LocalSurfaceColor 46 | import com.builtwithpaper.currentScreenWidthBreakpoint 47 | import com.builtwithpaper.isAtLeast 48 | import com.builtwithpaper.ScreenWidthBreakpoint 49 | import com.builtwithpaper.LocalOnSurfaceColor 50 | import com.builtwithpaper.TabItem 51 | 52 | @Composable 53 | fun Settings() { 54 | val breakpoint = currentScreenWidthBreakpoint() 55 | 56 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().background(LocalBackgroundColor.current)) { 57 | val breakpoint = currentScreenWidthBreakpoint() 58 | 59 | CompositionLocalProvider(LocalContentColor provides LocalOnBackgroundColor.current) { 60 | TopAppBar( 61 | modifier = Modifier.height(56.dp).fillMaxWidth(), 62 | title = { 63 | Text("Settings", modifier = Modifier.padding(top = 0.dp, end = 0.dp, bottom = 0.dp, start = 12.dp), fontSize = 24.sp, fontWeight = FontWeight(700), maxLines = 99) 64 | }, 65 | contentColor = LocalContentColor.current, 66 | backgroundColor = Color.Unspecified, 67 | contentPadding = PaddingValues(start = 0.dp, top = 4.dp, end = 0.dp, bottom = 4.dp) 68 | ) 69 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().padding(top = 16.dp, end = 16.dp, bottom = 0.dp, start = 16.dp)) { 70 | Column(modifier = Modifier.widthIn(max = 600.dp).fillMaxWidth().weight(weight = 1f, fill = false).fillMaxHeight().verticalScroll(rememberScrollState()).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp)) { 71 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)).padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp)) { 72 | Image(rememberAsyncImagePainter("https://images.unsplash.com/photo-1499996860823-5214fcc65f8f?q=80&w=3466&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"), contentDescription = "", modifier = Modifier.width(58.dp).height(58.dp).clip(RoundedCornerShape(100.dp)), contentScale = ContentScale.Crop) 73 | Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(1f, fill = false).fillMaxWidth()) { 74 | Text("Daniel", modifier = Modifier.fillMaxWidth(), fontSize = 16.sp, maxLines = 99) 75 | Text("+44 75 12 49384", modifier = Modifier.fillMaxWidth().alpha(0.6f), fontSize = 16.sp, maxLines = 99) 76 | } 77 | PrimaryButton(onClick = { /* TODO Handle this */ }, contentPadding = PaddingValues(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 8.dp)) { 78 | Text("Edit", fontSize = 16.sp, maxLines = 99) 79 | } 80 | } 81 | Column(modifier = Modifier.fillMaxWidth()) { 82 | Text("General", modifier = Modifier.padding(16.dp), fontSize = 16.sp, fontWeight = FontWeight(500), color = LocalPrimaryColor.current, maxLines = 99) 83 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current))) { 84 | LucideIcon("file-video", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 85 | Text("Notifications", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 86 | } 87 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current))) { 88 | LucideIcon("moon", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 89 | Text("Appearance", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 90 | } 91 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current))) { 92 | LucideIcon("lock-keyhole", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 93 | Text("Privacy", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 94 | } 95 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current))) { 96 | LucideIcon("cloud-cog", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 97 | Text("Storage & Data", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 98 | } 99 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current))) { 100 | LucideIcon("info", contentDescription = "", tint = LocalPrimaryColor.current, modifier = Modifier.padding(16.dp)) 101 | Text("About", modifier = Modifier.padding(top = 16.dp, end = 0.dp, bottom = 16.dp, start = 0.dp), fontSize = 16.sp, maxLines = 99) 102 | } 103 | } 104 | } 105 | } 106 | BottomNavigation(modifier = Modifier.heightIn(56.dp).fillMaxWidth().shadow(4.dp, RectangleShape).zIndex(4f).background(if (breakpoint isAtLeast ScreenWidthBreakpoint.Small) LocalSurfaceColor.current else Color.Unspecified), contentColor = LocalOnSurfaceColor.current, backgroundColor = LocalSurfaceColor.current) { 107 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 108 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 109 | LucideIcon("house", contentDescription = "", tint = LocalContentColor.current) 110 | Text("Chats", fontSize = 16.sp, maxLines = 99) 111 | } 112 | } 113 | TabItem(selected = false, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 114 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 115 | LucideIcon("contact", contentDescription = "", tint = LocalContentColor.current) 116 | Text("Contacts", fontSize = 16.sp, maxLines = 99) 117 | } 118 | } 119 | TabItem(selected = true, onSelected = { /* TODO Handle this */ }, modifier = Modifier.weight(1f, fill = false).fillMaxWidth().clip(RoundedCornerShape(LocalCornerRadius.current)), selectedColor = LocalPrimaryColor.current, contentColor = LocalContentColor.current) { 120 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.CenterVertically), modifier = Modifier.fillMaxWidth()) { 121 | LucideIcon("settings", contentDescription = "", tint = LocalContentColor.current) 122 | Text("Settings", fontSize = 16.sp, maxLines = 99) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/builtwithpaper/ContainerSize.ios.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalComposeUiApi::class) 2 | 3 | package com.builtwithpaper 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.ExperimentalComposeUiApi 7 | import androidx.compose.ui.platform.LocalDensity 8 | import androidx.compose.ui.platform.LocalWindowInfo 9 | import androidx.compose.ui.unit.DpSize 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | actual fun currentContainerSize(): DpSize { 14 | return with(LocalDensity.current) { 15 | val intSize = LocalWindowInfo.current.containerSize 16 | DpSize(intSize.width.dp, intSize.height.dp) 17 | } 18 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/builtwithpaper/LightIndication.kt: -------------------------------------------------------------------------------- 1 | package com.builtwithpaper 2 | 3 | import androidx.compose.foundation.IndicationNodeFactory 4 | import androidx.compose.foundation.interaction.FocusInteraction 5 | import androidx.compose.foundation.interaction.HoverInteraction 6 | import androidx.compose.foundation.interaction.InteractionSource 7 | import androidx.compose.foundation.interaction.PressInteraction 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 11 | import androidx.compose.ui.node.DelegatableNode 12 | import androidx.compose.ui.node.DrawModifierNode 13 | import androidx.compose.ui.node.invalidateDraw 14 | import kotlinx.coroutines.launch 15 | 16 | object LightIndication : IndicationNodeFactory { 17 | 18 | override fun create(interactionSource: InteractionSource): DelegatableNode = 19 | DefaultDebugIndicationInstance(interactionSource) 20 | 21 | override fun hashCode(): Int = -1 22 | 23 | override fun equals(other: Any?) = other === this 24 | 25 | private class DefaultDebugIndicationInstance(private val interactionSource: InteractionSource) : Modifier.Node(), 26 | DrawModifierNode { 27 | private var isPressed = false 28 | private var isHovered = false 29 | private var isFocused = false 30 | override fun onAttach() { 31 | coroutineScope.launch { 32 | var pressCount = 0 33 | var hoverCount = 0 34 | var focusCount = 0 35 | interactionSource.interactions.collect { interaction -> 36 | when (interaction) { 37 | is PressInteraction.Press -> pressCount++ 38 | is PressInteraction.Release -> pressCount-- 39 | is PressInteraction.Cancel -> pressCount-- 40 | is HoverInteraction.Enter -> hoverCount++ 41 | is HoverInteraction.Exit -> hoverCount-- 42 | is FocusInteraction.Focus -> focusCount++ 43 | is FocusInteraction.Unfocus -> focusCount-- 44 | } 45 | val pressed = pressCount > 0 46 | val hovered = hoverCount > 0 47 | val focused = focusCount > 0 48 | var invalidateNeeded = false 49 | if (isPressed != pressed) { 50 | isPressed = pressed 51 | invalidateNeeded = true 52 | } 53 | if (isHovered != hovered) { 54 | isHovered = hovered 55 | invalidateNeeded = true 56 | } 57 | if (isFocused != focused) { 58 | isFocused = focused 59 | invalidateNeeded = true 60 | } 61 | if (invalidateNeeded) invalidateDraw() 62 | } 63 | } 64 | } 65 | 66 | override fun ContentDrawScope.draw() { 67 | drawContent() 68 | if (isPressed) { 69 | drawRect(color = Color.Companion.White.copy(alpha = 0.5f), size = size) 70 | } else if (isHovered || isFocused) { 71 | drawRect(color = Color.Companion.White.copy(alpha = 0.3f), size = size) 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/example/MainViewController.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | 5 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | 7 | #Android 8 | android.nonTransitiveRClass=true 9 | android.useAndroidX=true 10 | 11 | #MPP 12 | kotlin.mpp.androidSourceSetLayoutVersion=2 13 | kotlin.mpp.enableCInteropCommonization=true 14 | 15 | #Development 16 | development=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.0" 3 | compose-multiplatform = "1.7.0" 4 | agp = "8.5.2" 5 | iconsLucide = "1.1.0" 6 | compose-unstyled = "1.20.0" 7 | materialRipple = "1.7.6" 8 | navigationCompose = "2.8.0-alpha10" 9 | addcoilmultiplatform = "3.0.0-rc01" 10 | android-compileSdk = "34" 11 | android-minSdk = "24" 12 | android-targetSdk = "34" 13 | androidx-activityCompose = "1.9.3" 14 | androidx-lifecycle = "2.8.3" 15 | 16 | [libraries] 17 | addcoilmultiplatform = { module = "com.alexstyl:addcoilmultiplatform", version.ref = "addcoilmultiplatform" } 18 | androidx-material-ripple = { module = "androidx.compose.material:material-ripple", version.ref = "materialRipple" } 19 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 20 | androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 21 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 22 | compose-unstyled = { group = "com.composables", name = "core", version.ref = "compose-unstyled" } 23 | icons-lucide = { module = "com.composables:icons-lucide", version.ref = "iconsLucide" } 24 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 25 | 26 | [plugins] 27 | androidApplication = { id = "com.android.application", version.ref = "agp" } 28 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 29 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 30 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.example 3 | APP_NAME=Bubbles -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 20 | 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | B92378962B6B1156000C7307 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 058557D7273AAEEB004C7B11 /* Preview Content */ = { 38 | isa = PBXGroup; 39 | children = ( 40 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, 41 | ); 42 | path = "Preview Content"; 43 | sourceTree = ""; 44 | }; 45 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | ); 49 | name = Frameworks; 50 | sourceTree = ""; 51 | }; 52 | 7555FF72242A565900829871 = { 53 | isa = PBXGroup; 54 | children = ( 55 | AB1DB47929225F7C00F7AF9C /* Configuration */, 56 | 7555FF7D242A565900829871 /* iosApp */, 57 | 7555FF7C242A565900829871 /* Products */, 58 | 42799AB246E5F90AF97AA0EF /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 7555FF7C242A565900829871 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 7555FF7B242A565900829871 /* KotlinProject.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 7555FF7D242A565900829871 /* iosApp */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 058557BA273AAA24004C7B11 /* Assets.xcassets */, 74 | 7555FF82242A565900829871 /* ContentView.swift */, 75 | 7555FF8C242A565B00829871 /* Info.plist */, 76 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 77 | 058557D7273AAEEB004C7B11 /* Preview Content */, 78 | ); 79 | path = iosApp; 80 | sourceTree = ""; 81 | }; 82 | AB1DB47929225F7C00F7AF9C /* Configuration */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | AB3632DC29227652001CCB65 /* Config.xcconfig */, 86 | ); 87 | path = Configuration; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 7555FF7A242A565900829871 /* iosApp */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 96 | buildPhases = ( 97 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 98 | 7555FF77242A565900829871 /* Sources */, 99 | B92378962B6B1156000C7307 /* Frameworks */, 100 | 7555FF79242A565900829871 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = iosApp; 107 | packageProductDependencies = ( 108 | ); 109 | productName = iosApp; 110 | productReference = 7555FF7B242A565900829871 /* KotlinProject.app */; 111 | productType = "com.apple.product-type.application"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | 7555FF73242A565900829871 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | BuildIndependentTargetsInParallel = YES; 120 | LastSwiftUpdateCheck = 1130; 121 | LastUpgradeCheck = 1540; 122 | ORGANIZATIONNAME = orgName; 123 | TargetAttributes = { 124 | 7555FF7A242A565900829871 = { 125 | CreatedOnToolsVersion = 11.3.1; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 130 | compatibilityVersion = "Xcode 14.0"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 7555FF72242A565900829871; 138 | packageReferences = ( 139 | ); 140 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 7555FF7A242A565900829871 /* iosApp */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 7555FF79242A565900829871 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, 155 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXShellScriptBuildPhase section */ 162 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { 163 | isa = PBXShellScriptBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | inputFileListPaths = ( 168 | ); 169 | inputPaths = ( 170 | ); 171 | name = "Compile Kotlin Framework"; 172 | outputFileListPaths = ( 173 | ); 174 | outputPaths = ( 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | shellPath = /bin/sh; 178 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; 179 | }; 180 | /* End PBXShellScriptBuildPhase section */ 181 | 182 | /* Begin PBXSourcesBuildPhase section */ 183 | 7555FF77242A565900829871 /* Sources */ = { 184 | isa = PBXSourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 188 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXSourcesBuildPhase section */ 193 | 194 | /* Begin XCBuildConfiguration section */ 195 | 7555FFA3242A565B00829871 /* Debug */ = { 196 | isa = XCBuildConfiguration; 197 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_NONNULL = YES; 201 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_ENABLE_OBJC_WEAK = YES; 207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 208 | CLANG_WARN_BOOL_CONVERSION = YES; 209 | CLANG_WARN_COMMA = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | }; 256 | name = Debug; 257 | }; 258 | 7555FFA4242A565B00829871 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_NONNULL = YES; 264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_ENABLE_OBJC_WEAK = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INFINITE_RECURSION = YES; 280 | CLANG_WARN_INT_CONVERSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 285 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 294 | ENABLE_NS_ASSERTIONS = NO; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | 7555FFA6242A565B00829871 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_IDENTITY = "Apple Development"; 320 | CODE_SIGN_STYLE = Automatic; 321 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 322 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 323 | ENABLE_PREVIEWS = YES; 324 | FRAMEWORK_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 327 | ); 328 | INFOPLIST_FILE = iosApp/Info.plist; 329 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 335 | PRODUCT_NAME = "${APP_NAME}"; 336 | PROVISIONING_PROFILE_SPECIFIER = ""; 337 | SWIFT_VERSION = 5.0; 338 | TARGETED_DEVICE_FAMILY = "1,2"; 339 | }; 340 | name = Debug; 341 | }; 342 | 7555FFA7242A565B00829871 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | CODE_SIGN_IDENTITY = "Apple Development"; 347 | CODE_SIGN_STYLE = Automatic; 348 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 349 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 350 | ENABLE_PREVIEWS = YES; 351 | FRAMEWORK_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 354 | ); 355 | INFOPLIST_FILE = iosApp/Info.plist; 356 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/Frameworks", 360 | ); 361 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 362 | PRODUCT_NAME = "${APP_NAME}"; 363 | PROVISIONING_PROFILE_SPECIFIER = ""; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 7555FFA3242A565B00829871 /* Debug */, 376 | 7555FFA4242A565B00829871 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 7555FFA6242A565B00829871 /* Debug */, 385 | 7555FFA7242A565B00829871 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | }; 392 | rootObject = 7555FF73242A565900829871 /* Project object */; 393 | } -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composablehorizons/bubbles/3ddce98d80963af3484423cf9a23c38760ec84b8/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Bubbles" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | mavenContent { 22 | includeGroupAndSubgroups("androidx") 23 | includeGroupAndSubgroups("com.android") 24 | includeGroupAndSubgroups("com.google") 25 | } 26 | } 27 | mavenCentral() 28 | } 29 | } 30 | 31 | include(":composeApp") --------------------------------------------------------------------------------