diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 00cec77..b6101ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,4 +56,6 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + implementation(project(":core")) } \ No newline at end of file diff --git a/app/src/main/java/com/example/uikit/MainActivity.kt b/app/src/main/java/com/example/uikit/MainActivity.kt index 4442e34..6c74ede 100644 --- a/app/src/main/java/com/example/uikit/MainActivity.kt +++ b/app/src/main/java/com/example/uikit/MainActivity.kt @@ -1,47 +1,413 @@ package com.example.uikit +import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.example.uikit.ui.theme.UikitTheme +import androidx.compose.ui.unit.dp +import com.example.core.component.button.AppButton +import com.example.core.component.button.BubbleButton +import com.example.core.component.button.BubbleButtonState +import com.example.core.component.button.ButtonState +import com.example.core.component.button.CartButton +import com.example.core.component.button.LoginButton +import com.example.core.component.button.OutlinedAppButton +import com.example.core.component.button.SecondaryAppButton +import com.example.core.component.card.CartCard +import com.example.core.component.card.PrimaryCard +import com.example.core.component.card.ProjectCard +import com.example.core.component.header.BigHeader +import com.example.core.component.header.SmallHeader +import com.example.core.component.input.EnterInputField +import com.example.core.component.search.AppSearchField +import com.example.core.component.select.AppSelector +import com.example.core.component.select.Option +import com.example.core.component.tabbar.BottomTabBar +import com.example.core.component.tabbar.Catalog +import com.example.core.component.tabbar.Home +import com.example.core.component.tabbar.Profile +import com.example.core.component.tabbar.Projects +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme class MainActivity : ComponentActivity() { + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - UikitTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) + CustomTheme { + Scaffold { tabPadding -> + var currentDestination by rememberSaveable { mutableStateOf("com.example.core.component.tabbar.Home") } + Scaffold( + bottomBar = { + BottomTabBar( + tabs = listOf(Home, Catalog, Projects, Profile), + hierarchy = sequenceOf(currentDestination), + onTabClick = { + currentDestination = it::class.qualifiedName.toString() + }, + bottomPadding = tabPadding.calculateBottomPadding() + ) + }, + containerColor = CustomTheme.colors.white + ) { + Column( + modifier = Modifier + .safeDrawingPadding() + .fillMaxSize() + .padding(horizontal = 16.dp) + .background(CustomTheme.colors.white) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy( + 15.dp, + alignment = Alignment.CenterVertically + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Инпуты", style = CustomTheme.typography.title1ExtraBold) + var fieldValue1 by rememberSaveable { mutableStateOf("") } + var errorMessage1 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue1, + onValueChange = { + fieldValue1 = it + errorMessage1 = "" + }, + errorMessage = errorMessage1, + label = "", + hint = "Введите имя" + ) + + var fieldValue2 by rememberSaveable { mutableStateOf("Иван") } + var errorMessage2 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue2, + onValueChange = { + fieldValue2 = it + errorMessage2 = "" + }, + errorMessage = errorMessage2, + label = "", + hint = "Введите имя" + ) + + var fieldValue3 by rememberSaveable { mutableStateOf("") } + var errorMessage3 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue3, + onValueChange = { + fieldValue3 = it + errorMessage3 = "" + }, + errorMessage = errorMessage3, + label = "Имя", + hint = "Введите имя" + ) + + var fieldValue4 by rememberSaveable { mutableStateOf("") } + var errorMessage4 by rememberSaveable { mutableStateOf("Введите ваше имя") } + EnterInputField( + value = fieldValue4, + onValueChange = { + fieldValue4 = it + errorMessage4 = "" + }, + errorMessage = errorMessage4, + label = "Имя", + hint = "Введите имя" + ) + + var fieldValue5 by rememberSaveable { mutableStateOf("") } + var errorMessage5 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue5, + onValueChange = { + fieldValue5 = it + errorMessage5 = "" + }, + errorMessage = errorMessage5, + label = "Имя", + hint = "Введите имя" + ) + + var fieldValue6 by rememberSaveable { mutableStateOf("Введите имя") } + var errorMessage6 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue6, + onValueChange = { + fieldValue6 = it + errorMessage6 = "" + }, + errorMessage = errorMessage6, + label = "Имя", + hint = "Введите имя" + ) + + var fieldValue7 by rememberSaveable { mutableStateOf("123456789") } + var errorMessage7 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue7, + onValueChange = { + fieldValue7 = it + errorMessage7 = "" + }, + errorMessage = errorMessage7, + label = "", + isPassword = true, + hint = "Введите имя" + ) + + var fieldValue8 by rememberSaveable { mutableStateOf("") } + var errorMessage8 by rememberSaveable { mutableStateOf("") } + EnterInputField( + value = fieldValue8, + onValueChange = { + fieldValue8 = it + errorMessage8 = "" + }, + errorMessage = errorMessage8, + label = "", + hint = "--.--.----" + ) + /* + * F6.user5@local.ru + * Pa$$w0rd + * */ + + Text(text = "Селекты", style = CustomTheme.typography.title1ExtraBold) + + var selectedOption1 by remember { mutableStateOf(null) } + + AppSelector( + selectedOption = selectedOption1, + options = listOf( + Option(label = "Мужской"), + Option(label = "Женский") + ), + onOptionSelect = { selectedOption1 = it }, + hint = "Пол" + ) + + var selectedOption2 by remember { mutableStateOf(Option("Мужской")) } + + AppSelector( + selectedOption = selectedOption2, + options = listOf( + Option(label = "Мужской"), + Option(label = "Женский") + ), + onOptionSelect = { selectedOption2 = it }, + hint = "Пол" + ) + + Text(text = "Поиск", style = CustomTheme.typography.title1ExtraBold) + var searchFieldValue by rememberSaveable { mutableStateOf("") } + + AppSearchField( + value = searchFieldValue, + onValueChange = { searchFieldValue = it }, + hint = "Искать описание" + ) + + Text(text = "Кнопки", style = CustomTheme.typography.title1ExtraBold) + + AppButton( + label = "Подтвердить", + onClick = {}, + state = ButtonState.Big + ) + + AppButton( + label = "Подтвердить", + onClick = {}, + state = ButtonState.Big, + enabled = false + ) + + OutlinedAppButton( + label = "Подтвердить", + onClick = {}, + state = ButtonState.Big + ) + + SecondaryAppButton( + label = "Подтвердить", + onClick = {}, + state = ButtonState.Big + ) + + AppButton( + label = "Добавить", + onClick = {}, + state = ButtonState.Small + ) + + OutlinedAppButton( + label = "Убрать", + onClick = {}, + state = ButtonState.Small + ) + + AppButton( + label = "Добавить", + onClick = {}, + state = ButtonState.Small, + enabled = false + ) + + SecondaryAppButton( + label = "Подтвердить", + onClick = {}, + state = ButtonState.Small + ) + + AppButton( + label = "Популярное", + onClick = {}, + state = ButtonState.Chips + ) + + AppButton( + label = "Популярное", + onClick = {}, + state = ButtonState.Chips, + enabled = false + ) + + CartButton( + onClick = {}, + totalPrice = 500 + ) + + LoginButton( + icon = CustomIcons.vk, + label = "Войти с VK", + onClick = {} + ) + + LoginButton( + icon = CustomIcons.yandex, + label = "Войти с Yandex", + onClick = {} + ) + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + BubbleButton( + state = BubbleButtonState.Small, + icon = CustomIcons.back + ) + + BubbleButton( + state = BubbleButtonState.Big, + icon = CustomIcons.filter + ) + } + + var inCartFirst by rememberSaveable { mutableStateOf(false) } + + Text(text = "Карточки", style = CustomTheme.typography.title1ExtraBold) + + PrimaryCard( + title = "Рубашка Воскресенье для машинного \n" + + "вязания", + category = "Мужская одежда", + price = 300, + inCart = inCartFirst, + onAddToCartClick = { inCartFirst = true }, + onRemoveFromCartClick = { inCartFirst = false }, + ) + + var inCartSecond by rememberSaveable { mutableStateOf(true) } + + PrimaryCard( + title = "Рубашка Воскресенье для машинного \n" + + "вязания", + category = "Мужская одежда", + price = 300, + inCart = inCartSecond, + onAddToCartClick = { inCartSecond = true }, + onRemoveFromCartClick = { inCartSecond = false }, + ) + + var currentQuantity by rememberSaveable { mutableIntStateOf(1) } + CartCard( + title = "Рубашка воскресенье для машинного вязания", + price = 300, + currentQuantity = currentQuantity, + onChangeQuantity = { currentQuantity = it } + ) + + ProjectCard( + title = "Мой первый проект", + dayElapsed = 2, + onOpenClick = {} + ) + + Text(text = "Хэдэр", style = CustomTheme.typography.title1ExtraBold) + + BigHeader( + title = "Корзина", + startContent = { + BubbleButton( + state = BubbleButtonState.Small, + icon = CustomIcons.back + ) + }, + endContent = { + Icon( + painter = CustomIcons.delete, + contentDescription = null, + tint = CustomTheme.colors.inputIcon + ) + } + ) + + SmallHeader( + title = "Корзина", + startContent = { + BubbleButton( + state = BubbleButtonState.Small, + icon = CustomIcons.back + ) + }, + endContent = { + Icon( + painter = CustomIcons.delete, + contentDescription = null, + tint = CustomTheme.colors.inputIcon + ) + } + ) + + Spacer(Modifier.height(100.dp)) + } + } } } } } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - UikitTheme { - Greeting("Android") - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/uikit/ui/theme/Color.kt b/app/src/main/java/com/example/uikit/ui/theme/Color.kt deleted file mode 100644 index 10a4fd3..0000000 --- a/app/src/main/java/com/example/uikit/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.uikit.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/example/uikit/ui/theme/Theme.kt b/app/src/main/java/com/example/uikit/ui/theme/Theme.kt deleted file mode 100644 index 9dad4cf..0000000 --- a/app/src/main/java/com/example/uikit/ui/theme/Theme.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.uikit.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun UikitTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/uikit/ui/theme/Type.kt b/app/src/main/java/com/example/uikit/ui/theme/Type.kt deleted file mode 100644 index 9ad83e5..0000000 --- a/app/src/main/java/com/example/uikit/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.uikit.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..0e2e50d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.serialization) apply false } \ No newline at end of file diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..e982578 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.example.core" + compileSdk = 35 + + defaultConfig { + minSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.kotlinx.serialization.json) + debugImplementation(libs.ui.tooling) + debugImplementation(libs.androidx.ui.tooling.preview) + api(platform(libs.androidx.compose.bom)) + api(libs.androidx.activity.compose) + api(libs.androidx.ui) + api(libs.androidx.ui.graphics) + api(libs.androidx.ui.tooling.preview) + api(libs.androidx.material3) + api(libs.material) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/src/androidTest/java/com/example/core/ComponentsTest.kt b/core/src/androidTest/java/com/example/core/ComponentsTest.kt new file mode 100644 index 0000000..1f2b6bc --- /dev/null +++ b/core/src/androidTest/java/com/example/core/ComponentsTest.kt @@ -0,0 +1,65 @@ +package com.example.core + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.core.component.button.AppButton +import com.example.core.component.button.ButtonState +import com.example.core.component.select.AppSelector +import com.example.core.component.select.Option +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class ComponentsTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun test_input_error() { + } + + @Test + fun test_app_selector() { + composeTestRule.setContent { + var selectedOption1 by remember { mutableStateOf(null) } + + AppSelector( + selectedOption = selectedOption1, + options = listOf( + Option(label = "Мужской"), + Option(label = "Женский") + ), + onOptionSelect = { selectedOption1 = it }, + hint = "Пол" + ) + } + + composeTestRule.onNodeWithText("Пол").performClick() + composeTestRule.onNodeWithTag("AppBottomSheet").assertIsDisplayed() + } + + @Test + fun test_chips_button() { + composeTestRule.setContent { + AppButton( + label = "Популярное", + onClick = {}, + state = ButtonState.Chips, + enabled = false + ) + } + + composeTestRule.onNodeWithText("Популярное").assertIsDisplayed() + } +} \ No newline at end of file diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/AppButton.kt b/core/src/main/java/com/example/core/component/button/AppButton.kt new file mode 100644 index 0000000..a634d00 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/AppButton.kt @@ -0,0 +1,60 @@ +package com.example.core.component.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun AppButton( + label: String, // Текст кнопки + onClick: () -> Unit, // Действие на клик + state: ButtonState, // Состояние + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Box(contentAlignment = Alignment.Center, modifier = modifier) { + val commonCondition = state is ButtonState.Chips + Button( + modifier = modifier + .then(state.width()) + .height(state.height()), + onClick = onClick, + shape = RoundedCornerShape(10.dp), + enabled = enabled, + colors = ButtonDefaults.buttonColors().copy( + containerColor = CustomTheme.colors.accent, + disabledContainerColor = if (commonCondition) CustomTheme.colors.inputBg else CustomTheme.colors.accentInactive + ) + ) { + } + Text( + text = label, + style = state.typography() + .copy( + color = if (commonCondition && !enabled) CustomTheme.colors.description else CustomTheme.colors.white, + textAlign = TextAlign.Center + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .then(state.width()) + .padding(horizontal = state.padding()), + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/BubbleButton.kt b/core/src/main/java/com/example/core/component/button/BubbleButton.kt new file mode 100644 index 0000000..aede5c3 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/BubbleButton.kt @@ -0,0 +1,35 @@ +package com.example.core.component.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.Painter +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun BubbleButton( + state: BubbleButtonState, // cостояние + icon: Painter, + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .size(state.size()) + .clip(RoundedCornerShape(state.roundness())) + .background(CustomTheme.colors.inputBg) + ) { + Icon(painter = icon, contentDescription = null, tint = CustomTheme.colors.description) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/BubbleButtonState.kt b/core/src/main/java/com/example/core/component/button/BubbleButtonState.kt new file mode 100644 index 0000000..201d8e7 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/BubbleButtonState.kt @@ -0,0 +1,39 @@ +package com.example.core.component.button + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +interface BubbleButtonState { + + @Composable + fun roundness(): Dp // радиус скругления + + @Composable + fun size(): Dp // размер + + object Small : BubbleButtonState { + + @Composable + override fun roundness(): Dp = CustomTheme.elevation.spacing8dp + + @Composable + override fun size(): Dp = CustomTheme.elevation.spacing32dp + } + + object Big : BubbleButtonState { + + @Composable + override fun roundness(): Dp = 10.dp + + @Composable + override fun size(): Dp = CustomTheme.elevation.spacing48dp + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/ButtonState.kt b/core/src/main/java/com/example/core/component/button/ButtonState.kt new file mode 100644 index 0000000..b30893a --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/ButtonState.kt @@ -0,0 +1,77 @@ +package com.example.core.component.button + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@SuppressLint("ModifierFactoryExtensionFunction") +interface ButtonState { + + fun width(): Modifier // т.к fillmaxWith не является конкретной шириной, то возвращаемый тип modifier + + fun height(): Dp // высота + + fun padding(): Dp // отступы основного текста кнопки от контейнера + + @Composable + fun typography(): TextStyle + + object Big : ButtonState { + + override fun width(): Modifier = Modifier.fillMaxWidth() + + override fun height(): Dp = 56.dp + + override fun padding(): Dp = 0.dp + + @Composable + override fun typography(): TextStyle = CustomTheme.typography.title3Semibold + } + + object Small : ButtonState { + + override fun width(): Modifier = Modifier.width(98.dp) + + override fun height(): Dp = 40.dp + + override fun padding(): Dp = 13.5.dp + + @Composable + override fun typography(): TextStyle = CustomTheme.typography.captionSemibold + } + + object Chips : ButtonState { + + override fun width(): Modifier = Modifier.width(129.dp) + + override fun height(): Dp = 48.dp + + override fun padding(): Dp = 20.dp + + @Composable + override fun typography(): TextStyle = CustomTheme.typography.textMedium + } + + object Login : ButtonState { + + override fun width(): Modifier = Modifier.fillMaxWidth() + + override fun height(): Dp = 60.dp + + override fun padding(): Dp = 0.dp + + @Composable + override fun typography(): TextStyle = CustomTheme.typography.title3Medium + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/CartButton.kt b/core/src/main/java/com/example/core/component/button/CartButton.kt new file mode 100644 index 0000000..6ce4909 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/CartButton.kt @@ -0,0 +1,73 @@ +package com.example.core.component.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.core.R +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun CartButton( + onClick: () -> Unit, + totalPrice: Int, // цена на кнопке + modifier: Modifier = Modifier +) { + val state = ButtonState.Big + + Box(contentAlignment = Alignment.CenterStart, modifier = modifier) { + Button( + modifier = modifier + .then(state.width()) + .height(state.height()), + onClick = onClick, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors().copy( + containerColor = CustomTheme.colors.accent, + disabledContentColor = CustomTheme.colors.accentInactive + ) + ) { + } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + + Icon( + painter = CustomIcons.cart, + contentDescription = null, + tint = CustomTheme.colors.white, + modifier = Modifier.padding(horizontal = CustomTheme.elevation.spacing16dp) + ) + + Text( + text = stringResource(R.string.in_cart), + style = state.typography().copy(color = CustomTheme.colors.white) + ) + + Spacer(Modifier.weight(1f)) + + Text( + text = stringResource(R.string.total_price, totalPrice), + style = state.typography().copy(color = CustomTheme.colors.white) + ) + Spacer(Modifier.width(CustomTheme.elevation.spacing16dp)) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/ChangeQuantityButton.kt b/core/src/main/java/com/example/core/component/button/ChangeQuantityButton.kt new file mode 100644 index 0000000..0dcc2d6 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/ChangeQuantityButton.kt @@ -0,0 +1,97 @@ +package com.example.core.component.button + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +/** + * для установки цвета и обработки крайних случаев изменения по значению + * в кнопке есть параметр количества */ +@Composable +fun ChangeQuantityButton( + currentQuantity: Int, + onIncrementClick: () -> Unit, // обработка увеличения количества + onDecrementClick: () -> Unit, // обработка уменьшения количества + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .width(CustomTheme.elevation.spacing64dp) + .height(CustomTheme.elevation.spacing32dp) + .clip(RoundedCornerShape(8.dp)) + .background(CustomTheme.colors.inputBg) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 6.dp) + ) { + val minusCondition = currentQuantity > 1 + val minusTint by animateColorAsState(if (minusCondition) CustomTheme.colors.placeholder else CustomTheme.colors.inputIcon) + Icon( + painter = CustomIcons.minus, + contentDescription = null, + tint = minusTint, + modifier = Modifier.then( + if (minusCondition) { + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + null, + onClick = onDecrementClick + ) + } else Modifier + ) + ) + + VerticalDivider( + thickness = 1.dp, + color = CustomTheme.colors.inputStroke, + modifier = Modifier.padding( + horizontal = 6.dp, + vertical = CustomTheme.elevation.spacing8dp + ) + ) + + val plusCondition = currentQuantity < 100 + val plusTint by animateColorAsState(if (plusCondition) CustomTheme.colors.placeholder else CustomTheme.colors.inputIcon) + + Icon( + painter = CustomIcons.plus, + contentDescription = null, + tint = plusTint, + modifier = Modifier.then( + if (plusCondition) { + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + null, + onClick = onIncrementClick + ) + } else Modifier + ) + ) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/LoginButton.kt b/core/src/main/java/com/example/core/component/button/LoginButton.kt new file mode 100644 index 0000000..1e1be63 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/LoginButton.kt @@ -0,0 +1,47 @@ +package com.example.core.component.button + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun LoginButton( + icon: Painter, + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val state = ButtonState.Login + + OutlinedButton( + modifier = modifier + .then(state.width()) + .height(state.height()), + onClick = onClick, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(width = 1.dp, color = CustomTheme.colors.inputStroke) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image(painter = icon, contentDescription = null) + Spacer(Modifier.width(CustomTheme.elevation.spacing16dp)) + Text(text = label, style = state.typography()) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/OutlinedAppButton.kt b/core/src/main/java/com/example/core/component/button/OutlinedAppButton.kt new file mode 100644 index 0000000..78b450f --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/OutlinedAppButton.kt @@ -0,0 +1,49 @@ +package com.example.core.component.button + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun OutlinedAppButton( + label: String, + onClick: () -> Unit, + state: ButtonState, + modifier: Modifier = Modifier +) { + Box(contentAlignment = Alignment.Center, modifier = modifier) { + OutlinedButton( + modifier = modifier + .then(state.width()) + .height(state.height()), + onClick = onClick, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(width = 1.dp, color = CustomTheme.colors.accent) + ) { + } + Text( + text = label, + style = state.typography() + .copy(color = CustomTheme.colors.accent, textAlign = TextAlign.Center), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.then(state.width()).padding(horizontal = state.padding()) + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/button/SecondaryAppButton.kt b/core/src/main/java/com/example/core/component/button/SecondaryAppButton.kt new file mode 100644 index 0000000..eedcb05 --- /dev/null +++ b/core/src/main/java/com/example/core/component/button/SecondaryAppButton.kt @@ -0,0 +1,56 @@ +package com.example.core.component.button + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun SecondaryAppButton( + label: String, + onClick: () -> Unit, + state: ButtonState, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Box(contentAlignment = Alignment.Center, modifier = modifier) { + Button( + modifier = modifier + .then(state.width()) + .height(state.height()), + onClick = onClick, + shape = RoundedCornerShape(10.dp), + enabled = enabled, + colors = ButtonDefaults.buttonColors().copy( + containerColor = CustomTheme.colors.inputBg, + disabledContentColor = CustomTheme.colors.accentInactive + ) + ) { + } + Text( + text = label, + style = state.typography() + .copy(color = CustomTheme.colors.black, textAlign = TextAlign.Center), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .then(state.width()) + .padding(horizontal = state.padding()) + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/card/AppCardBackground.kt b/core/src/main/java/com/example/core/component/card/AppCardBackground.kt new file mode 100644 index 0000000..5a344d4 --- /dev/null +++ b/core/src/main/java/com/example/core/component/card/AppCardBackground.kt @@ -0,0 +1,45 @@ +package com.example.core.component.card + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +/** + * общий фон для карточек */ +@Composable +fun AppCardBackground( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Column( + modifier = modifier + .fillMaxWidth() + .shadow( + elevation = 20.dp, + shape = RoundedCornerShape(CustomTheme.elevation.spacing12dp), + clip = false, + spotColor = Color(0xFFE4E8F5).copy(alpha = 0.6f) + ) + .clip(RoundedCornerShape(CustomTheme.elevation.spacing12dp)) + .background(CustomTheme.colors.white) + ) { + Box(modifier = Modifier.padding(CustomTheme.elevation.spacing16dp)) { + content() + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/card/CartCard.kt b/core/src/main/java/com/example/core/component/card/CartCard.kt new file mode 100644 index 0000000..5829d10 --- /dev/null +++ b/core/src/main/java/com/example/core/component/card/CartCard.kt @@ -0,0 +1,76 @@ +package com.example.core.component.card + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.core.R +import com.example.core.component.button.ChangeQuantityButton +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun CartCard( + title: String, + price: Int, + currentQuantity: Int, + onChangeQuantity: (Int) -> Unit, + modifier: Modifier = Modifier +) { + AppCardBackground(modifier = modifier) { + Column { + Row { + Text( + text = title, + style = CustomTheme.typography.headlineMedium, + modifier = Modifier.weight(2f) + ) + + Row(horizontalArrangement = Arrangement.End, modifier = Modifier.weight(1f)) { + Icon( + painter = CustomIcons.close, + contentDescription = null, + tint = CustomTheme.colors.description + ) + } + } + Spacer(Modifier.height(34.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.total_price, price), + style = CustomTheme.typography.title3Medium, + modifier = Modifier.weight(1.7f) + ) + + Text( + text = stringResource(R.string.cart_card_quantity, currentQuantity), + style = CustomTheme.typography.textRegular + ) + + Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.End) { + /** + * обработка количества уже без дополнительных проверок */ + ChangeQuantityButton( + currentQuantity = currentQuantity, + onIncrementClick = { onChangeQuantity(currentQuantity.inc()) }, + onDecrementClick = { onChangeQuantity(currentQuantity.dec()) } + ) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/card/PrimaryCard.kt b/core/src/main/java/com/example/core/component/card/PrimaryCard.kt new file mode 100644 index 0000000..95ef680 --- /dev/null +++ b/core/src/main/java/com/example/core/component/card/PrimaryCard.kt @@ -0,0 +1,69 @@ +package com.example.core.component.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.example.core.R +import com.example.core.component.button.AppButton +import com.example.core.component.button.ButtonState +import com.example.core.component.button.OutlinedAppButton +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun PrimaryCard( + title: String, + category: String, + price: Int, + inCart: Boolean, + onAddToCartClick: () -> Unit, + onRemoveFromCartClick: () -> Unit, + modifier: Modifier = Modifier +) { + AppCardBackground(modifier = modifier) { + Column { + Text(text = title, style = CustomTheme.typography.headlineMedium) + Spacer(Modifier.height(CustomTheme.elevation.spacing16dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = category, + style = CustomTheme.typography.captionSemibold.copy( + color = CustomTheme.colors.placeholder + ) + ) + + Spacer(Modifier.height(CustomTheme.elevation.spacing4dp)) + Text( + text = stringResource(R.string.total_price, price), + style = CustomTheme.typography.title3Semibold + ) + } + + if (inCart) { + OutlinedAppButton( + label = stringResource(R.string.drop), + onClick = onRemoveFromCartClick, + state = ButtonState.Small + ) + } else { + AppButton( + label = stringResource(R.string.add), + onClick = onAddToCartClick, + state = ButtonState.Small + ) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/card/ProjectCard.kt b/core/src/main/java/com/example/core/component/card/ProjectCard.kt new file mode 100644 index 0000000..4dfbafd --- /dev/null +++ b/core/src/main/java/com/example/core/component/card/ProjectCard.kt @@ -0,0 +1,52 @@ +package com.example.core.component.card + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.core.R +import com.example.core.component.button.AppButton +import com.example.core.component.button.ButtonState +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun ProjectCard( + title: String, + dayElapsed: Int, + onOpenClick: () -> Unit, + modifier: Modifier = Modifier +) { + AppCardBackground(modifier = modifier) { + Column { + Text(text = title, style = CustomTheme.typography.headlineMedium) + Spacer(Modifier.height(44.dp)) + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = pluralStringResource(R.plurals.elapsed_days, dayElapsed, dayElapsed), + style = CustomTheme.typography.captionSemibold.copy( + color = CustomTheme.colors.placeholder + ), + modifier = Modifier.weight(1f) + ) + + AppButton( + label = stringResource(R.string.open), + onClick = onOpenClick, + state = ButtonState.Small + ) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/header/BigHeader.kt b/core/src/main/java/com/example/core/component/header/BigHeader.kt new file mode 100644 index 0000000..2ff9fb5 --- /dev/null +++ b/core/src/main/java/com/example/core/component/header/BigHeader.kt @@ -0,0 +1,37 @@ +package com.example.core.component.header + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun BigHeader( + title: String, + modifier: Modifier = Modifier, + startContent: @Composable () -> Unit = {}, + endContent: @Composable () -> Unit = {} +) { + Column(modifier = modifier) { + Box(contentAlignment = Alignment.CenterEnd, modifier = modifier.fillMaxWidth()) { + Box(modifier = Modifier.align(Alignment.CenterStart)) { + startContent() + } + + endContent() + } + Spacer(Modifier.height(CustomTheme.elevation.spacing24dp)) + Text(text = title, style = CustomTheme.typography.title1ExtraBold) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/header/SmallHeader.kt b/core/src/main/java/com/example/core/component/header/SmallHeader.kt new file mode 100644 index 0000000..59303f0 --- /dev/null +++ b/core/src/main/java/com/example/core/component/header/SmallHeader.kt @@ -0,0 +1,35 @@ +package com.example.core.component.header + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun SmallHeader( + title: String, + modifier: Modifier = Modifier, + startContent: @Composable () -> Unit, + endContent: @Composable () -> Unit +) { + Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + + Box(modifier = Modifier.align(Alignment.CenterStart)) { + startContent() + } + + Text(text = title, style = CustomTheme.typography.title1SemiBold) + + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + endContent() + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/input/EnterInputField.kt b/core/src/main/java/com/example/core/component/input/EnterInputField.kt new file mode 100644 index 0000000..5f3b87f --- /dev/null +++ b/core/src/main/java/com/example/core/component/input/EnterInputField.kt @@ -0,0 +1,177 @@ +package com.example.core.component.input + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun EnterInputField( + value: String, + onValueChange: (String) -> Unit, + label: String, // названия поля (надпись над полем) + hint: String, // подсказка + errorMessage: String, // сообщенме об ошибке + modifier: Modifier = Modifier, + isPassword: Boolean = false, // являяется ли паролем (для отображения и скрытия) + keyboardOptions: KeyboardOptions = KeyboardOptions.Default // смена типа клавиатуры (только цифры и тд) +) { + + /** + * отслеживание состояния фокуса для обводки активного поля + * */ + val focusRequester = remember { FocusRequester() } + + var hasFocus by rememberSaveable { mutableStateOf(false) } + + var showPassword by rememberSaveable { mutableStateOf(false) } + + /** + * Установка цвета рамки в зависимости от состояния + * */ + val borderColor by animateColorAsState( + when { + errorMessage.isNotBlank() -> CustomTheme.colors.error + errorMessage.isBlank() && hasFocus -> CustomTheme.colors.accent + value.isEmpty() || (value.isNotEmpty() && label.isNotEmpty()) || isPassword -> CustomTheme.colors.inputStroke + else -> CustomTheme.colors.inputIcon + } + ) + + /** + * установка цвета поверх конентка в случае ошибки + * */ + val overlayErrorColor by animateColorAsState( + if (errorMessage.isNotBlank()) CustomTheme.colors.error.copy(alpha = 0.1f) else Color.Transparent + ) + + Column(modifier = modifier) { + if (label.isNotBlank()) { + Text( + text = label, + style = CustomTheme.typography.captionRegular.copy( + color = CustomTheme.colors.description + ) + ) + + Spacer(Modifier.height(CustomTheme.elevation.spacing8dp)) + } + + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .fillMaxWidth() + .height(CustomTheme.elevation.spacing48dp) + .clip(RoundedCornerShape(10.dp)) + .background(CustomTheme.colors.inputBg) + .border(width = 1.dp, color = borderColor, shape = RoundedCornerShape(10.dp)) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(overlayErrorColor) + ) + + Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.width(14.dp)) + + Box(contentAlignment = Alignment.CenterStart, modifier = Modifier.weight(1f)) { + + if (value.isEmpty()) { + Text( + text = hint, + style = CustomTheme.typography.textRegular.copy( + color = CustomTheme.colors.placeholder + ) + ) + } + + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + cursorBrush = SolidColor(CustomTheme.colors.accent), + textStyle = CustomTheme.typography.textRegular, + keyboardOptions = keyboardOptions, + visualTransformation = if (isPassword && !showPassword) { + PasswordVisualTransformation() + } else VisualTransformation.None, + modifier = Modifier + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + hasFocus = focusState.hasFocus + } + ) + } + + if (isPassword) { + val icon = + if (showPassword) CustomIcons.showPassword else CustomIcons.hidePassword + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { showPassword = !showPassword } + ) + Spacer(Modifier.width(15.dp)) + } + } + } + + /** + * отображение текста ошибки в случае ее наличия + * */ + AnimatedVisibility(visible = errorMessage.isNotBlank()) { + Column { + Spacer(Modifier.height(CustomTheme.elevation.spacing8dp)) + Text( + text = errorMessage, + style = CustomTheme.typography.captionRegular.copy( + color = CustomTheme.colors.error + ) + ) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/search/AppSearchField.kt b/core/src/main/java/com/example/core/component/search/AppSearchField.kt new file mode 100644 index 0000000..32a1abb --- /dev/null +++ b/core/src/main/java/com/example/core/component/search/AppSearchField.kt @@ -0,0 +1,116 @@ +package com.example.core.component.search + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@Composable +fun AppSearchField( + value: String, + onValueChange: (String) -> Unit, + hint: String, + modifier: Modifier = Modifier +) { + val focusRequester = remember { FocusRequester() } + + var hasFocus by rememberSaveable { mutableStateOf(false) } + + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .fillMaxWidth() + .height(CustomTheme.elevation.spacing48dp) + .clip(RoundedCornerShape(10.dp)) + .background(CustomTheme.colors.inputBg) + .border( + width = 1.dp, + color = CustomTheme.colors.inputStroke, + shape = RoundedCornerShape(10.dp) + ) + ) { + Column(modifier = modifier) { + Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.width(14.dp)) + Icon( + painter = CustomIcons.search, + tint = CustomTheme.colors.description, + contentDescription = null + ) + Spacer(Modifier.width(CustomTheme.elevation.spacing8dp)) + + Box(contentAlignment = Alignment.CenterStart, modifier = Modifier.weight(1f)) { + + if (value.isEmpty()) { + Text( + text = hint, + style = CustomTheme.typography.textRegular.copy( + color = CustomTheme.colors.placeholder + ) + ) + } + + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + cursorBrush = SolidColor(CustomTheme.colors.accent), + textStyle = CustomTheme.typography.textRegular, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + hasFocus = focusState.hasFocus + } + ) + } + if (hasFocus) { + Icon( + painter = CustomIcons.close, + contentDescription = null, + tint = CustomTheme.colors.description, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onValueChange("") } + ) + + Spacer(Modifier.width(14.dp)) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/select/AppBottomSheet.kt b/core/src/main/java/com/example/core/component/select/AppBottomSheet.kt new file mode 100644 index 0000000..b78e679 --- /dev/null +++ b/core/src/main/java/com/example/core/component/select/AppBottomSheet.kt @@ -0,0 +1,61 @@ +package com.example.core.component.select + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +/** + * общее диалоговое окно +* */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppBottomSheet( + sheetState: SheetState, // состояние, для управлением отображения + onDismissRequest: () -> Unit, // обработка запроса на закрытие + modifier: Modifier = Modifier, + content: @Composable () -> Unit // контент +) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = onDismissRequest, + dragHandle = null, + containerColor = CustomTheme.colors.white, + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Spacer(Modifier.height(CustomTheme.elevation.spacing24dp)) + Box { + Image( + painter = CustomIcons.closeSheet, + contentDescription = null, + modifier = Modifier.align(Alignment.TopEnd) + ) + + content() + } + Spacer(Modifier.height(84.dp)) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/component/select/AppSelector.kt b/core/src/main/java/com/example/core/component/select/AppSelector.kt new file mode 100644 index 0000000..28229a9 --- /dev/null +++ b/core/src/main/java/com/example/core/component/select/AppSelector.kt @@ -0,0 +1,143 @@ +package com.example.core.component.select + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.example.core.theme.CustomIcons +import com.example.core.theme.CustomTheme +import kotlinx.coroutines.launch + +/** + * Автор: Манякин Дмитрий (user5) + * Дата создания: 26.05.2025 + * */ + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppSelector( + selectedOption: Option?, // выбранная опция, если null то отображается подсказка + options: List