diff --git a/mobile/kmp/gradle/libs.versions.toml b/mobile/kmp/gradle/libs.versions.toml index 9552d8258..40c85d346 100644 --- a/mobile/kmp/gradle/libs.versions.toml +++ b/mobile/kmp/gradle/libs.versions.toml @@ -1,13 +1,17 @@ [versions] +calendar = "2.9.0" kotlin = "2.2.0" ktor = "3.3.1" compose = "1.10.0-rc02" composeShadow = "2.0.4" coilCompose = "3.3.0" +kotlinxDatetime = "0.6.1" agp = "8.9.0" androidx-activityCompose = "1.9.3" [libraries] +compose-calendar = { module = "com.kizitonwose.calendar:compose-multiplatform", version.ref = "calendar" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } compose-shadow = { module = "com.adamglin:compose-shadow", version.ref = "composeShadow" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } diff --git a/mobile/kmp/sample/src/commonMain/kotlin/com/atls/hyperion/sample/App.kt b/mobile/kmp/sample/src/commonMain/kotlin/com/atls/hyperion/sample/App.kt index 4678edce3..cfcc0b92e 100644 --- a/mobile/kmp/sample/src/commonMain/kotlin/com/atls/hyperion/sample/App.kt +++ b/mobile/kmp/sample/src/commonMain/kotlin/com/atls/hyperion/sample/App.kt @@ -9,7 +9,11 @@ import com.atls.hyperion.ui.components.card.stories.CardStory import com.atls.hyperion.ui.components.checkbox.stories.CheckboxStory import com.atls.hyperion.ui.components.divider.stories.DividerStory import com.atls.hyperion.ui.components.input.stories.InputStory +import com.atls.hyperion.ui.components.modal.bottom.stories.BottomDialogStory +import com.atls.hyperion.ui.components.modal.popup.stories.PopupStory import com.atls.hyperion.ui.components.switch.stories.SwitchStory +import com.atls.hyperion.ui.fragment.datepicker.stories.DatePickerStory +import com.atls.hyperion.ui.fragment.datepicker.stories.DateRangePickerStory import com.atls.hyperion.ui.primitives.stories.LinkStory import com.atls.hyperion.ui.primitives.stories.TextStory @@ -19,10 +23,14 @@ fun App() { Storybook( components = listOf( AvatarStory(), + BottomDialogStory(), ButtonStory(), CheckboxStory(), + DatePickerStory(), + DateRangePickerStory(), DividerStory(), InputStory(), + PopupStory(), SwitchStory(), CardStory(), TextStory(), diff --git a/mobile/kmp/storybook/src/commonMain/kotlin/com/atls/hyperion/storybook/shared/ui/ComponentVariants.kt b/mobile/kmp/storybook/src/commonMain/kotlin/com/atls/hyperion/storybook/shared/ui/ComponentVariants.kt index c0e6bb68b..dab73d455 100644 --- a/mobile/kmp/storybook/src/commonMain/kotlin/com/atls/hyperion/storybook/shared/ui/ComponentVariants.kt +++ b/mobile/kmp/storybook/src/commonMain/kotlin/com/atls/hyperion/storybook/shared/ui/ComponentVariants.kt @@ -3,8 +3,10 @@ package com.atls.hyperion.storybook.shared.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider @@ -55,6 +57,7 @@ fun ComponentVariants( fontSize = FontSize.small, modifier = Modifier.padding(top = Padding.tiny) ) + Spacer(Modifier.width(Padding.small)) content(appearanceProvider(), shapeProvider()) } Divider() diff --git a/mobile/kmp/ui/build.gradle.kts b/mobile/kmp/ui/build.gradle.kts index 48a72d736..fcb039b21 100644 --- a/mobile/kmp/ui/build.gradle.kts +++ b/mobile/kmp/ui/build.gradle.kts @@ -34,12 +34,15 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) + implementation(compose.material3) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.compose.shadow) implementation(libs.coil.compose) implementation(libs.coil.network.ktor) + implementation(libs.compose.calendar) + api(libs.kotlinx.datetime) } androidMain { diff --git a/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_left.xml b/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_left.xml new file mode 100644 index 000000000..62b41a877 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_right.xml b/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_right.xml new file mode 100644 index 000000000..3979fa801 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/composeResources/drawable/chevron_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/Layout.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/Layout.kt index 5e41c0509..3077aaab7 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/Layout.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/Layout.kt @@ -23,7 +23,7 @@ import com.atls.hyperion.ui.components.button.styles.appearance.Colors import com.atls.hyperion.ui.components.button.styles.shape.ButtonShape import com.atls.hyperion.ui.shared.addon.AddonPosition import com.atls.hyperion.ui.shared.addon.AddonSlotManager -import com.atls.hyperion.ui.theme.tokens.Elevation +import com.atls.hyperion.ui.theme.tokens.layout.Elevation import com.atls.hyperion.ui.theme.tokens.colors.Colors as ThemeColors @OptIn(ExperimentalMaterialApi::class) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/styles/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/styles/appearance/Appearance.kt index 5a2048b9e..1c1b0db9c 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/styles/appearance/Appearance.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/button/styles/appearance/Appearance.kt @@ -2,7 +2,7 @@ package com.atls.hyperion.ui.components.button.styles.appearance import androidx.compose.ui.unit.Dp import com.atls.hyperion.ui.components.button.state.ButtonState -import com.atls.hyperion.ui.theme.tokens.Elevation +import com.atls.hyperion.ui.theme.tokens.layout.Elevation data class ButtonAppearance( val default: Colors, diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/card/style/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/card/style/appearance/Appearance.kt index 8af36bcc7..1f874708c 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/card/style/appearance/Appearance.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/card/style/appearance/Appearance.kt @@ -2,7 +2,7 @@ package com.atls.hyperion.ui.components.card.style.appearance import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp -import com.atls.hyperion.ui.theme.tokens.Elevation +import com.atls.hyperion.ui.theme.tokens.layout.Elevation import com.atls.hyperion.ui.theme.tokens.colors.Colors as ThemeColors data class CardAppearance( diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/checkbox/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/checkbox/Component.kt index f09cb7708..f0f1c2bf7 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/checkbox/Component.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/checkbox/Component.kt @@ -16,7 +16,7 @@ import com.atls.hyperion.ui.components.checkbox.locals.LocalState import com.atls.hyperion.ui.components.checkbox.state.State import com.atls.hyperion.ui.components.checkbox.styles.appearance.CheckboxAppearance import com.atls.hyperion.ui.components.checkbox.styles.shape.CheckboxShape -import com.atls.hyperion.ui.primitives.Icon +import com.atls.hyperion.ui.primitives.icon.Icon import com.atls.hyperion.ui.theme.tokens.layout.Weight @Composable diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/horizontal/HorizontalDivider.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/horizontal/Component.kt similarity index 100% rename from mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/horizontal/HorizontalDivider.kt rename to mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/horizontal/Component.kt diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/vertical/VerticalDivider.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/vertical/Component.kt similarity index 100% rename from mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/vertical/VerticalDivider.kt rename to mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/divider/vertical/Component.kt diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/Input.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/Input.kt new file mode 100644 index 000000000..fb0fcaa94 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/Input.kt @@ -0,0 +1,86 @@ +package com.atls.hyperion.ui.components.input + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import com.atls.hyperion.ui.components.input.container.InputContainer +import com.atls.hyperion.ui.components.input.state.InputState +import com.atls.hyperion.ui.components.input.style.appearance.InputAppearance +import com.atls.hyperion.ui.components.input.style.shape.InputShape +import com.atls.hyperion.ui.shared.addon.AddonSlotManager +import com.atls.hyperion.ui.theme.tokens.layout.Weight +import com.atls.hyperion.ui.theme.tokens.colors.Colors as ThemeColors + +@Composable +fun Input( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + isError: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + appearance: InputAppearance, + shape: InputShape, + visualTransformation: VisualTransformation = VisualTransformation.None, + addons: AddonSlotManager = AddonSlotManager(), + placeholder: @Composable (() -> Unit)? = null, +) { + val isFocused = interactionSource.collectIsFocusedAsState().value + val isPressed = interactionSource.collectIsPressedAsState().value + + val currentState = when { + !enabled -> InputState.Disabled + isError -> InputState.Error + isPressed -> InputState.Active + isFocused -> InputState.Focused + value.text.isNotEmpty() -> InputState.Filled + else -> InputState.Default + } + + val colors = appearance.getColorsFromState(currentState) + + InputContainer( + modifier = modifier, + appearance = appearance, + shape = shape, + state = currentState, + addons = addons + ) { + BasicTextField( + value = value, + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + interactionSource = interactionSource, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cursorBrush = SolidColor(colors.cursorColor), + textStyle = shape.typography.copy(color = colors.textColor), + visualTransformation = visualTransformation, + decorationBox = { innerTextField -> + if (value.text.isEmpty() && placeholder != null) { + placeholder() + } + innerTextField() + }, + modifier = Modifier + .background(ThemeColors.Palette.transparent) + .padding(shape.textPaddings) + .weight(Weight.full) + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/container/InputContainer.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/container/InputContainer.kt new file mode 100644 index 000000000..e6de4f7ad --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/container/InputContainer.kt @@ -0,0 +1,76 @@ +package com.atls.hyperion.ui.components.input.container + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.components.input.locals.LocalAppearance +import com.atls.hyperion.ui.components.input.locals.LocalState +import com.atls.hyperion.ui.components.input.state.InputState +import com.atls.hyperion.ui.components.input.style.appearance.InputAppearance +import com.atls.hyperion.ui.components.input.style.appearance.blue +import com.atls.hyperion.ui.components.input.style.shape.InputShape +import com.atls.hyperion.ui.components.input.style.shape.normal +import com.atls.hyperion.ui.shared.addon.AddonPosition +import com.atls.hyperion.ui.shared.addon.AddonSlotManager + +@Composable +fun InputContainer( + modifier: Modifier = Modifier, + appearance: InputAppearance = InputAppearance.blue(), + shape: InputShape = InputShape.normal(), + state: InputState? = null, + addons: AddonSlotManager = AddonSlotManager(), + content: @Composable RowScope.(interactionSource: MutableInteractionSource) -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isFocused = interactionSource.collectIsFocusedAsState().value + val isPressed = interactionSource.collectIsPressedAsState().value + + val resolvedState = state ?: when { + isPressed -> InputState.Active + isFocused -> InputState.Focused + else -> InputState.Default + } + + val colors = appearance.getColorsFromState(resolvedState) + + CompositionLocalProvider( + LocalState provides resolvedState, + LocalAppearance provides appearance + ) { + Row( + modifier = modifier + .border( + width = shape.borderStroke, + color = colors.borderColor, + shape = RoundedCornerShape(shape.cornerRadius) + ) + .background(colors.backgroundColor, RoundedCornerShape(shape.cornerRadius)) + .padding(shape.paddings), + verticalAlignment = Alignment.CenterVertically + ) { + addons.get(AddonPosition.Before).forEach { + it.Content() + it.Spacer() + } + + content(interactionSource) + + addons.get(AddonPosition.After).forEach { + it.Spacer() + it.Content() + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalAppearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalAppearance.kt new file mode 100644 index 000000000..3203205af --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalAppearance.kt @@ -0,0 +1,8 @@ +package com.atls.hyperion.ui.components.input.locals + +import androidx.compose.runtime.compositionLocalOf +import com.atls.hyperion.ui.components.input.style.appearance.InputAppearance + +val LocalAppearance = compositionLocalOf { + error("InputAppearance not provided") +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalState.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalState.kt new file mode 100644 index 000000000..70e3ac986 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/locals/LocalState.kt @@ -0,0 +1,8 @@ +package com.atls.hyperion.ui.components.input.locals + +import androidx.compose.runtime.compositionLocalOf +import com.atls.hyperion.ui.components.input.state.InputState + +val LocalState = compositionLocalOf { + error("InputState not provided") +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/placeholder/InputPlaceholder.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/placeholder/InputPlaceholder.kt new file mode 100644 index 000000000..9bc572171 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/placeholder/InputPlaceholder.kt @@ -0,0 +1,24 @@ +package com.atls.hyperion.ui.components.input.placeholder + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.components.input.style.appearance.InputAppearance +import com.atls.hyperion.ui.components.input.style.appearance.blue +import com.atls.hyperion.ui.components.input.style.shape.InputShape +import com.atls.hyperion.ui.components.input.style.shape.normal + +@Composable +fun InputPlaceholder( + modifier: Modifier = Modifier, + text: String, + appearance: InputAppearance = InputAppearance.blue(), + shape: InputShape = InputShape.normal() +) { + Text( + modifier = modifier, + text = text, + color = appearance.default.textColor, + style = shape.typography + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/state/InputState.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/state/InputState.kt new file mode 100644 index 000000000..8d53590f6 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/state/InputState.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.components.input.state + +sealed interface InputState { + data object Default : InputState + data object Focused : InputState + data object Filled : InputState + data object Disabled : InputState + data object Error : InputState + data object Active : InputState +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/stories/Component.kt new file mode 100644 index 000000000..e2ee2c9f0 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/stories/Component.kt @@ -0,0 +1,94 @@ +package com.atls.hyperion.ui.components.input.stories + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.TextFieldValue +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.storybook.shared.ui.ComponentVariants +import com.atls.hyperion.ui.components.input.Input +import com.atls.hyperion.ui.components.input.placeholder.InputPlaceholder +import com.atls.hyperion.ui.components.input.style.appearance.InputAppearance +import com.atls.hyperion.ui.components.input.style.appearance.blue +import com.atls.hyperion.ui.components.input.style.appearance.white +import com.atls.hyperion.ui.components.input.style.shape.InputShape +import com.atls.hyperion.ui.components.input.style.shape.large +import com.atls.hyperion.ui.components.input.style.shape.normal +import com.atls.hyperion.ui.primitives.HorizontalSpacer +import com.atls.hyperion.ui.primitives.VerticalSpacer +import com.atls.hyperion.ui.theme.tokens.layout.Space +import com.atls.hyperion.ui.theme.tokens.layout.Weight + +class InputStory : ComponentExample { + override val name: String = "Input" + + @Composable + override fun Content() { + var enabled by remember { mutableStateOf(true) } + var isError by remember { mutableStateOf(false) } + var textValue by remember { mutableStateOf(TextFieldValue("")) } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Space.g12), + verticalAlignment = Alignment.CenterVertically + ) { + Text(modifier = Modifier.weight(Weight.full), text = "Enabled") + HorizontalSpacer(Space.g12) + Switch(checked = enabled, onCheckedChange = { enabled = it }) + } + VerticalSpacer(Space.g8) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Space.g12), + verticalAlignment = Alignment.CenterVertically + ) { + Text(modifier = Modifier.weight(Weight.full), text = "Error") + HorizontalSpacer(Space.g12) + Switch(checked = isError, onCheckedChange = { isError = it }) + } + VerticalSpacer(Space.g12) + + ComponentVariants( + name = "Input", + appearances = listOf( + "Blue" to { InputAppearance.blue() }, + "White" to { InputAppearance.white() } + ), + shapes = listOf( + "Large" to { InputShape.large() }, + "Normal" to { InputShape.normal() } + ) + ) { appearance: InputAppearance, shape: InputShape -> + Input( + value = textValue, + onValueChange = { textValue = it }, + appearance = appearance, + shape = shape, + enabled = enabled, + isError = isError, + placeholder = { + InputPlaceholder( + text = "Placeholder", + appearance = appearance, + shape = shape + ) + } + ) + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Appearance.kt new file mode 100644 index 000000000..155d9c6dd --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Appearance.kt @@ -0,0 +1,24 @@ +package com.atls.hyperion.ui.components.input.style.appearance + +import com.atls.hyperion.ui.components.input.state.InputState + +data class InputAppearance( + val default: Colors, + val filled: Colors, + val focused: Colors, + val disabled: Colors, + val error: Colors, + val active: Colors +) { + companion object Companion + + fun getColorsFromState(state: InputState): Colors = + when (state) { + InputState.Default -> default + InputState.Disabled -> disabled + InputState.Error -> error + InputState.Filled -> filled + InputState.Focused -> focused + InputState.Active -> active + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Colors.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Colors.kt new file mode 100644 index 000000000..9397dd7c5 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Colors.kt @@ -0,0 +1,20 @@ +package com.atls.hyperion.ui.components.input.style.appearance + +import androidx.compose.ui.graphics.Color +import com.atls.hyperion.ui.theme.tokens.colors.ColorSet +import com.atls.hyperion.ui.theme.tokens.colors.Colors as ThemeColors + +data class Colors( + val backgroundColor: Color, + val textColor: Color, + val borderColor: Color = ThemeColors.Palette.transparent, + val cursorColor: Color = textColor +) { + companion object { + fun fromColorSet(colorSet: ColorSet): Colors = Colors( + backgroundColor = colorSet.background, + textColor = colorSet.font, + borderColor = colorSet.border + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Variants.kt new file mode 100644 index 000000000..32f59a713 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/appearance/Variants.kt @@ -0,0 +1,26 @@ +package com.atls.hyperion.ui.components.input.style.appearance + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.theme.tokens.colors.Colors as TokenColors + +@Composable +fun InputAppearance.Companion.blue(): InputAppearance = + InputAppearance( + default = Colors.fromColorSet(TokenColors.Input.Blue.Default), + filled = Colors.fromColorSet(TokenColors.Input.Blue.Default), + focused = Colors.fromColorSet(TokenColors.Input.Blue.Focus), + disabled = Colors.fromColorSet(TokenColors.Input.Blue.Disabled), + error = Colors.fromColorSet(TokenColors.Input.Blue.Default), + active = Colors.fromColorSet(TokenColors.Input.Blue.Active) + ) + +@Composable +fun InputAppearance.Companion.white(): InputAppearance = + InputAppearance( + default = Colors.fromColorSet(TokenColors.Input.White.Default), + filled = Colors.fromColorSet(TokenColors.Input.White.Default), + focused = Colors.fromColorSet(TokenColors.Input.White.Focus), + disabled = Colors.fromColorSet(TokenColors.Input.White.Disabled), + error = Colors.fromColorSet(TokenColors.Input.White.Default), + active = Colors.fromColorSet(TokenColors.Input.White.Active) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Shape.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Shape.kt new file mode 100644 index 000000000..daab415a8 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Shape.kt @@ -0,0 +1,16 @@ +package com.atls.hyperion.ui.components.input.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import com.atls.hyperion.ui.theme.tokens.layout.Space + +data class InputShape( + val cornerRadius: Dp, + val borderStroke: Dp, + val paddings: PaddingValues, + val textPaddings: PaddingValues = PaddingValues(Space.zero), + val typography: TextStyle +) { + companion object Companion +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Variants.kt new file mode 100644 index 000000000..504233cea --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/input/style/shape/Variants.kt @@ -0,0 +1,38 @@ +package com.atls.hyperion.ui.components.input.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import com.atls.hyperion.ui.theme.tokens.layout.BorderStroke +import com.atls.hyperion.ui.theme.tokens.layout.CornerRadius +import com.atls.hyperion.ui.theme.tokens.layout.Space +import com.atls.hyperion.ui.theme.typography.FontSize + +@Composable +fun InputShape.Companion.large(): InputShape = + InputShape( + cornerRadius = CornerRadius.md, + borderStroke = BorderStroke.tiny, + paddings = PaddingValues( + vertical = Space.g12, + horizontal = Space.g16 + ), + textPaddings = PaddingValues(Space.zero), + typography = TextStyle(fontSize = FontSize.md) + ) + +@Composable +fun InputShape.Companion.normal(): InputShape = + InputShape( + cornerRadius = CornerRadius.xs3, + borderStroke = BorderStroke.tiny, + paddings = PaddingValues( + vertical = Space.g8, + horizontal = Space.g12 + ), + textPaddings = PaddingValues( + vertical = Space.g4, + horizontal = Space.zero + ), + typography = TextStyle(fontSize = FontSize.sm) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/Component.kt new file mode 100644 index 000000000..f691e5306 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/Component.kt @@ -0,0 +1,59 @@ +package com.atls.hyperion.ui.components.modal.bottom + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.DragHandle +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.appearance.DragHandleAppearance +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.appearance.default +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.shape.DragHandleShape +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.shape.default +import com.atls.hyperion.ui.components.modal.style.appearance.ModalAppearance +import com.atls.hyperion.ui.components.modal.style.appearance.default +import com.atls.hyperion.ui.components.modal.style.shape.ModalShape +import com.atls.hyperion.ui.components.modal.style.shape.bottom + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomDialog( + modifier: Modifier = Modifier, + appearance: ModalAppearance = ModalAppearance.default(), + shape: ModalShape = ModalShape.bottom(), + dragHandleShape: DragHandleShape = DragHandleShape.default(), + dragHandleAppearance: DragHandleAppearance = DragHandleAppearance.default(), + dragHandle: @Composable () -> Unit = { + DragHandle( + appearance = dragHandleAppearance, + shape = dragHandleShape + ) + }, + sheetState: SheetState, + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) { + ModalBottomSheet( + modifier = modifier + .windowInsetsPadding(WindowInsets.statusBars), + onDismissRequest = onDismissRequest, + sheetState = sheetState, + shape = shape.shape, + dragHandle = dragHandle, + containerColor = appearance.backgroundColor, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(shape.paddings) + ) { + content() + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/Component.kt new file mode 100644 index 000000000..ecb695f41 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/Component.kt @@ -0,0 +1,29 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.runtime.Composable +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.appearance.DragHandleAppearance +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.shape.DragHandleShape + +@Composable +fun DragHandle( + appearance: DragHandleAppearance, + shape: DragHandleShape +) { + Box( + modifier = Modifier + .padding(shape.paddings) + .width(shape.width) + .height(shape.height) + .background( + color = appearance.backgroundColor, + shape = RoundedCornerShape(shape.cornerRadius) + ) + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/Size.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/Size.kt new file mode 100644 index 000000000..a442f8a74 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/Size.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle.style + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +object DragHandleSize { + val height: Dp = 6.dp + val width: Dp = 79.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Appearance.kt new file mode 100644 index 000000000..14cea132b --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Appearance.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.appearance + +import androidx.compose.ui.graphics.Color + +data class DragHandleAppearance( + val backgroundColor: Color +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Variants.kt new file mode 100644 index 000000000..b6b63ace1 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/appearance/Variants.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.appearance + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.theme.tokens.colors.Colors + +@Composable +fun DragHandleAppearance.Companion.default(): DragHandleAppearance = + DragHandleAppearance( + backgroundColor = Colors.Palette.gray + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Shape.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Shape.kt new file mode 100644 index 000000000..78edfd16a --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Shape.kt @@ -0,0 +1,13 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.Dp + +data class DragHandleShape( + val height: Dp, + val width: Dp, + val cornerRadius: Dp, + val paddings: PaddingValues +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Variants.kt new file mode 100644 index 000000000..ed2d95e21 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/dragHandle/style/shape/Variants.kt @@ -0,0 +1,16 @@ +package com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.components.modal.bottom.dragHandle.style.DragHandleSize +import com.atls.hyperion.ui.theme.tokens.layout.CornerRadius +import com.atls.hyperion.ui.theme.tokens.layout.Space + +@Composable +fun DragHandleShape.Companion.default(): DragHandleShape = + DragHandleShape( + height = DragHandleSize.height, + width = DragHandleSize.width, + cornerRadius = CornerRadius.xl6, + paddings = PaddingValues(top = Space.g8) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/stories/Component.kt new file mode 100644 index 000000000..8ce2156a5 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/bottom/stories/Component.kt @@ -0,0 +1,77 @@ +package com.atls.hyperion.ui.components.modal.bottom.stories + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +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.setValue +import androidx.compose.ui.Modifier +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.ui.components.button.Button +import com.atls.hyperion.ui.components.button.styles.appearance.ButtonAppearance +import com.atls.hyperion.ui.components.button.styles.appearance.blue +import com.atls.hyperion.ui.components.button.styles.shape.ButtonShape +import com.atls.hyperion.ui.components.button.styles.shape.normal +import com.atls.hyperion.ui.components.modal.bottom.BottomDialog +import com.atls.hyperion.ui.theme.tokens.layout.Space +import kotlinx.coroutines.launch + +class BottomDialogStory : ComponentExample { + override val name: String = "BottomDialog" + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + var showDialog by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Space.g12) + ) { + Button( + text = "Show Bottom Dialog", + appearance = ButtonAppearance.blue(), + shape = ButtonShape.normal(), + onClick = { showDialog = true } + ) + + if (showDialog) { + BottomDialog( + sheetState = sheetState, + onDismissRequest = { showDialog = false } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Space.g16) + ) { + Text(text = "This is a Bottom Dialog") + Button( + modifier = Modifier.padding(top = Space.g12), + text = "Close", + appearance = ButtonAppearance.blue(), + shape = ButtonShape.normal(), + onClick = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + showDialog = false + } + } + } + ) + } + } + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/Popup.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/Popup.kt new file mode 100644 index 000000000..f5f5cd0f8 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/Popup.kt @@ -0,0 +1,43 @@ +package com.atls.hyperion.ui.components.modal.popup + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.atls.hyperion.ui.components.modal.style.appearance.ModalAppearance +import com.atls.hyperion.ui.components.modal.style.appearance.default +import com.atls.hyperion.ui.components.modal.style.shape.ModalShape +import com.atls.hyperion.ui.components.modal.style.shape.popup + +@Composable +fun Popup( + modifier: Modifier = Modifier, + appearance: ModalAppearance = ModalAppearance.default(), + shape: ModalShape = ModalShape.popup(), + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false + ) + ) { + Box( + modifier = modifier + .padding(shape.spacers) + .background( + color = appearance.backgroundColor, + shape = shape.shape + ) + .fillMaxWidth() + .padding(shape.paddings) + ) { + content() + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/stories/Component.kt new file mode 100644 index 000000000..378158877 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/popup/stories/Component.kt @@ -0,0 +1,63 @@ +package com.atls.hyperion.ui.components.modal.popup.stories + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.ui.components.button.Button +import com.atls.hyperion.ui.components.button.styles.appearance.ButtonAppearance +import com.atls.hyperion.ui.components.button.styles.appearance.blue +import com.atls.hyperion.ui.components.button.styles.shape.ButtonShape +import com.atls.hyperion.ui.components.button.styles.shape.normal +import com.atls.hyperion.ui.components.modal.popup.Popup +import com.atls.hyperion.ui.theme.tokens.layout.Space + +class PopupStory : ComponentExample { + override val name: String = "Popup" + + @Composable + override fun Content() { + var showDialog by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Space.g12) + ) { + Button( + text = "Show Popup", + appearance = ButtonAppearance.blue(), + shape = ButtonShape.normal(), + onClick = { showDialog = true } + ) + + if (showDialog) { + Popup( + onDismissRequest = { showDialog = false } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(Space.g16) + ) { + Text(text = "This is a Popup Dialog") + Button( + modifier = Modifier.padding(top = Space.g12), + text = "Close", + appearance = ButtonAppearance.blue(), + shape = ButtonShape.normal(), + onClick = { showDialog = false } + ) + } + } + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Appearance.kt new file mode 100644 index 000000000..45aa7714d --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Appearance.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.components.modal.style.appearance + +import androidx.compose.ui.graphics.Color + +data class ModalAppearance( + val backgroundColor: Color +) { + companion object Companion +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Variants.kt new file mode 100644 index 000000000..52bafdf81 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/appearance/Variants.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.components.modal.style.appearance + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.theme.tokens.colors.Colors + +@Composable +fun ModalAppearance.Companion.default(): ModalAppearance = + ModalAppearance( + backgroundColor = Colors.Palette.white + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Shape.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Shape.kt new file mode 100644 index 000000000..5245e4d98 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Shape.kt @@ -0,0 +1,14 @@ +package com.atls.hyperion.ui.components.modal.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp + +data class ModalShape( + val shape: Shape, + val shadowElevation: Dp, + val paddings: PaddingValues, + val spacers: PaddingValues +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Variants.kt new file mode 100644 index 000000000..dd33044dc --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/modal/style/shape/Variants.kt @@ -0,0 +1,34 @@ +package com.atls.hyperion.ui.components.modal.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.theme.tokens.layout.CornerRadius +import com.atls.hyperion.ui.theme.tokens.layout.Elevation +import com.atls.hyperion.ui.theme.tokens.layout.Space + +@Composable +fun ModalShape.Companion.popup(): ModalShape = + ModalShape( + shape = RoundedCornerShape(CornerRadius.zero), + shadowElevation = Elevation.tiny, + paddings = PaddingValues(Space.g20), + spacers = PaddingValues(horizontal = Space.g24) + ) + +@Composable +fun ModalShape.Companion.bottom(): ModalShape = + ModalShape( + shape = RoundedCornerShape( + topStart = CornerRadius.xl4, + topEnd = CornerRadius.xl4, + ), + shadowElevation = Elevation.zero, + paddings = PaddingValues( + top = Space.g4, + bottom = Space.g20, + start = Space.g20, + end = Space.g20 + ), + spacers = PaddingValues(horizontal = Space.zero) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/Switch.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/Switch.kt new file mode 100644 index 000000000..32c7d9df1 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/Switch.kt @@ -0,0 +1,80 @@ +package com.atls.hyperion.ui.components.switch + +import androidx.compose.animation.core.animateDpAsState +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.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.atls.hyperion.ui.components.switch.state.SwitchState +import com.atls.hyperion.ui.components.switch.styles.appearance.SwitchAppearance +import com.atls.hyperion.ui.components.switch.styles.appearance.default +import com.atls.hyperion.ui.components.switch.styles.shape.SwitchShape +import com.atls.hyperion.ui.components.switch.styles.shape.medium + +@Composable +fun Switch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + disabled: Boolean = false, + appearance: SwitchAppearance = SwitchAppearance.default(), + shape: SwitchShape = SwitchShape.medium(), +) { + val state = remember(checked, disabled) { + if (disabled) { + SwitchState.Disabled + } else { + if (checked) SwitchState.Checked else SwitchState.Default + } + } + + val colors = appearance.fromState(state) + + val thumbOffset by animateDpAsState( + targetValue = if (checked) shape.trackWidth - shape.thumbSize - shape.thumbPadding else shape.thumbPadding + ) + + Box( + modifier = modifier + .size(width = shape.trackWidth, height = shape.trackHeight) + .clip(RoundedCornerShape(shape.trackHeight / 2)) + .background(colors.trackColor) + .border( + width = 1.dp, + color = colors.trackBorderColor, + shape = RoundedCornerShape(shape.trackHeight / 2) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = !disabled + ) { + onCheckedChange(!checked) + }, + contentAlignment = Alignment.CenterStart + ) { + Box( + modifier = Modifier + .offset(x = thumbOffset) + .size(shape.thumbSize) + .clip(RoundedCornerShape(shape.thumbSize / 2)) + .background(colors.thumbColor) + .border( + width = 1.dp, + color = colors.thumbBorderColor, + shape = RoundedCornerShape(shape.thumbSize / 2) + ) + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/state/SwitchState.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/state/SwitchState.kt new file mode 100644 index 000000000..659acc8e4 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/state/SwitchState.kt @@ -0,0 +1,7 @@ +package com.atls.hyperion.ui.components.switch.state + +enum class SwitchState { + Default, + Checked, + Disabled +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/stories/Component.kt new file mode 100644 index 000000000..5015f5c81 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/stories/Component.kt @@ -0,0 +1,69 @@ +package com.atls.hyperion.ui.components.switch.stories + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.storybook.shared.ui.ComponentVariants +import com.atls.hyperion.ui.components.switch.Switch +import com.atls.hyperion.ui.components.switch.styles.appearance.SwitchAppearance +import com.atls.hyperion.ui.components.switch.styles.appearance.default +import com.atls.hyperion.ui.components.switch.styles.shape.SwitchShape +import com.atls.hyperion.ui.components.switch.styles.shape.medium +import com.atls.hyperion.ui.primitives.HorizontalSpacer +import com.atls.hyperion.ui.primitives.VerticalSpacer +import com.atls.hyperion.ui.theme.tokens.layout.Space +import com.atls.hyperion.ui.theme.tokens.layout.Weight + +class SwitchStory : ComponentExample { + override val name: String = "Switch" + + @Composable + override fun Content() { + var checked by remember { mutableStateOf(false) } + var enabled by remember { mutableStateOf(true) } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Space.g12), + verticalAlignment = Alignment.CenterVertically + ) { + Text(modifier = Modifier.weight(Weight.full), text = "Enabled") + HorizontalSpacer(Space.g12) + androidx.compose.material.Switch( + checked = enabled, + onCheckedChange = { enabled = it } + ) + } + VerticalSpacer(Space.g8) + ComponentVariants( + name = "Switch", + appearances = listOf( + "Primary" to { SwitchAppearance.default() } + ), + shapes = listOf( + "Medium" to { SwitchShape.medium() } + ) + ) { appearance: SwitchAppearance, shape: SwitchShape -> + Switch( + checked = checked, + disabled = !enabled, + appearance = appearance, + shape = shape, + onCheckedChange = { checked = it } + ) + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/Size.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/Size.kt new file mode 100644 index 000000000..d0be7b9ec --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/Size.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.components.switch.styles + +import androidx.compose.ui.unit.dp + +object SwitchSize { + val defaultWidth = 44.dp + val defaultHeight = 22.dp + val defaultTrackSize = 18.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Appearance.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Appearance.kt new file mode 100644 index 000000000..d2dc4a8d0 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Appearance.kt @@ -0,0 +1,19 @@ +package com.atls.hyperion.ui.components.switch.styles.appearance + +import com.atls.hyperion.ui.components.switch.state.SwitchState + +data class SwitchAppearance( + val default: Colors, + val checked: Colors = default, + val disabled: Colors = default +) { + fun fromState(state: SwitchState): Colors { + return when (state) { + SwitchState.Default -> default + SwitchState.Checked -> checked + SwitchState.Disabled -> disabled + } + } + + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Colors.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Colors.kt new file mode 100644 index 000000000..75eac9fac --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Colors.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.components.switch.styles.appearance + +import androidx.compose.ui.graphics.Color + +data class Colors( + val trackColor: Color, + val trackBorderColor: Color, + val thumbColor: Color, + val thumbBorderColor: Color, +) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Variants.kt new file mode 100644 index 000000000..b61709f33 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/appearance/Variants.kt @@ -0,0 +1,19 @@ +package com.atls.hyperion.ui.components.switch.styles.appearance + +import com.atls.hyperion.ui.theme.tokens.colors.Colors as ThemeColors + +fun SwitchAppearance.Companion.default(): SwitchAppearance = + SwitchAppearance( + default = Colors( + trackColor = ThemeColors.Palette.transparent, + trackBorderColor = ThemeColors.Palette.blue, + thumbColor = ThemeColors.Palette.blue, + thumbBorderColor = ThemeColors.Palette.blue + ), + disabled = Colors( + trackColor = ThemeColors.Palette.transparent, + trackBorderColor = ThemeColors.Palette.blue, + thumbColor = ThemeColors.Palette.transparent, + thumbBorderColor = ThemeColors.Palette.blue + ) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Shape.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Shape.kt new file mode 100644 index 000000000..dec93f9d9 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Shape.kt @@ -0,0 +1,12 @@ +package com.atls.hyperion.ui.components.switch.styles.shape + +import androidx.compose.ui.unit.Dp + +data class SwitchShape( + val trackWidth: Dp, + val trackHeight: Dp, + val thumbSize: Dp, + val thumbPadding: Dp +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Variants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Variants.kt new file mode 100644 index 000000000..bda4ed823 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/components/switch/styles/shape/Variants.kt @@ -0,0 +1,12 @@ +package com.atls.hyperion.ui.components.switch.styles.shape + +import com.atls.hyperion.ui.components.switch.styles.SwitchSize +import com.atls.hyperion.ui.theme.tokens.layout.Space + +fun SwitchShape.Companion.medium(): SwitchShape = + SwitchShape( + trackWidth = SwitchSize.defaultWidth, + trackHeight = SwitchSize.defaultHeight, + thumbSize = SwitchSize.defaultTrackSize, + thumbPadding = Space.g2 + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/Fragment.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/Fragment.kt new file mode 100644 index 000000000..6fa36b2df --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/Fragment.kt @@ -0,0 +1,33 @@ +package com.atls.hyperion.ui.fragment.datepicker + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.components.modal.popup.Popup +import com.atls.hyperion.ui.fragment.datepicker.model.DateSelection +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.variants.default +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.fragment.datepicker.style.shape.variants.default +import com.atls.hyperion.ui.fragment.datepicker.ui.DatePickerContent + +@Composable +fun DatePicker( + modifier: Modifier = Modifier, + selection: DateSelection, + onSelectionChange: (DateSelection) -> Unit, + onDismiss: () -> Unit, + appearance: DatePickerAppearance = DatePickerAppearance.default(), + shape: DatePickerShape = DatePickerShape.default() +) { + Popup( + modifier = modifier, + onDismissRequest = onDismiss + ) { + DatePickerContent( + selection = selection, + onSelectionChange = onSelectionChange, + appearance = appearance, + shape = shape + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/config/Constants.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/config/Constants.kt new file mode 100644 index 000000000..e86eadb6f --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/config/Constants.kt @@ -0,0 +1,4 @@ +package com.atls.hyperion.ui.fragment.datepicker.config + +internal val DAYS_IN_WEEK = 7 +internal val WEEK_RANGE = 0..6 diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextDateRange.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextDateRange.kt new file mode 100644 index 000000000..cb65aa690 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextDateRange.kt @@ -0,0 +1,16 @@ +package com.atls.hyperion.ui.fragment.datepicker.lib + +import com.atls.hyperion.ui.fragment.datepicker.model.DateSelection +import kotlinx.datetime.LocalDate + +fun DateSelection.Range.next(clicked: LocalDate): DateSelection.Range = + when { + start == null || end != null -> + copy(start = clicked, end = null) + + clicked < start -> + copy(start = clicked, end = start) + + else -> + copy(end = clicked) + } diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextMonth.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextMonth.kt new file mode 100644 index 000000000..7d054307f --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/NextMonth.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.fragment.datepicker.lib + +import kotlinx.datetime.Month +import kotlinx.datetime.YearMonth + +fun YearMonth.next(): YearMonth = + if (month == Month.DECEMBER) + YearMonth(year + 1, Month.JANUARY) + else + YearMonth(year, Month.entries[month.ordinal + 1]) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/PreviousMonth.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/PreviousMonth.kt new file mode 100644 index 000000000..1610c86fb --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/lib/PreviousMonth.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.fragment.datepicker.lib + +import kotlinx.datetime.Month +import kotlinx.datetime.YearMonth + +fun YearMonth.previous(): YearMonth = + if (month == Month.JANUARY) + YearMonth(year - 1, Month.DECEMBER) + else + YearMonth(year, Month.entries[month.ordinal - 1]) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DateSelection.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DateSelection.kt new file mode 100644 index 000000000..2996cd71c --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DateSelection.kt @@ -0,0 +1,15 @@ +package com.atls.hyperion.ui.fragment.datepicker.model + +import kotlinx.datetime.LocalDate + +sealed interface DateSelection { + + data class Single( + val date: LocalDate? = null + ) : DateSelection + + data class Range( + val start: LocalDate? = null, + val end: LocalDate? = null + ) : DateSelection +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DividerPosition.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DividerPosition.kt new file mode 100644 index 000000000..a6340af9e --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/model/DividerPosition.kt @@ -0,0 +1,5 @@ +package com.atls.hyperion.ui.fragment.datepicker.model + +enum class DividerPosition { + TOP, BOTTOM, NONE +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/stories/Component.kt new file mode 100644 index 000000000..39b06502e --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/stories/Component.kt @@ -0,0 +1,69 @@ +package com.atls.hyperion.ui.fragment.datepicker.stories + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.ui.fragment.datepicker.DatePicker +import com.atls.hyperion.ui.fragment.datepicker.model.DateSelection + +class DatePickerStory( + override val name: String = "Date picker" +) : ComponentExample { + + @Composable + override fun Content() { + var selection by remember { + mutableStateOf(DateSelection.Single(null)) + } + + Column(Modifier.padding(16.dp)) { + Text( + text = when (val s = selection) { + is DateSelection.Single -> "Selected: ${s.date ?: "null"}" + else -> "Unexpected state" + } + ) + + DatePicker( + selection = selection, + onSelectionChange = { selection = it }, + onDismiss = {} + ) + } + } +} + +class DateRangePickerStory( + override val name: String = "Date range picker" +) : ComponentExample { + + @Composable + override fun Content() { + var selection by remember { + mutableStateOf(DateSelection.Range(null, null)) + } + + Column(Modifier.padding(16.dp)) { + Text( + text = when (val s = selection) { + is DateSelection.Range -> "From: ${s.start ?: "null"} To: ${s.end ?: "null"}" + else -> "Unexpected state" + } + ) + + DatePicker( + selection = selection, + onSelectionChange = { selection = it }, + onDismiss = {} + ) + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Cell.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Cell.kt new file mode 100644 index 000000000..bca22879b --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Cell.kt @@ -0,0 +1,18 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance + +import androidx.compose.ui.graphics.Color + +data class CellAppearance( + val backgroundColor: Color, + val textColor: Color, + val activeBackgroundColor: Color, + val activeTextColor: Color, + val inRangeBackgroundColor: Color, + val inRangeTextColor: Color, + val borderColor: Color = Color.Transparent, + val activeBorderColor: Color = Color.Transparent, + val inRangeBorderColor: Color = Color.Transparent, + val headerTextColor: Color +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Header.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Header.kt new file mode 100644 index 000000000..f1b5fc284 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Header.kt @@ -0,0 +1,10 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance + +import androidx.compose.ui.graphics.Color + +data class HeaderAppearance( + val textColor: Color, + val arrowColor: Color +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Picker.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Picker.kt new file mode 100644 index 000000000..a6f0ee657 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/Picker.kt @@ -0,0 +1,17 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance + +import androidx.compose.ui.graphics.Color +import com.atls.hyperion.ui.components.divider.style.appearance.DividerAppearance +import com.atls.hyperion.ui.components.modal.style.appearance.ModalAppearance + +data class DatePickerAppearance( + val cellAppearance: CellAppearance, + val headerAppearance: HeaderAppearance, + val weekDaysAppearance: WeekDaysAppearance, + val backgroundColor: Color, + val dividerColor: Color, + val dividerAppearance: DividerAppearance, + val modalAppearance: ModalAppearance +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/WeekDays.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/WeekDays.kt new file mode 100644 index 000000000..8f0b78e56 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/WeekDays.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance + +import androidx.compose.ui.graphics.Color + +data class WeekDaysAppearance( + val textColor: Color +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Cell.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Cell.kt new file mode 100644 index 000000000..0119c7649 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Cell.kt @@ -0,0 +1,17 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance.variants + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.CellAppearance +import com.atls.hyperion.ui.theme.tokens.colors.Colors as TokenColors + +@Composable +fun CellAppearance.Companion.default(): CellAppearance = + CellAppearance( + backgroundColor = TokenColors.Palette.transparent, + textColor = TokenColors.Text.black, + activeBackgroundColor = TokenColors.Palette.blue, + activeTextColor = TokenColors.Text.white, + inRangeBackgroundColor = TokenColors.Palette.lightPurple, + inRangeTextColor = TokenColors.Text.black, + headerTextColor = TokenColors.Text.gray + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Header.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Header.kt new file mode 100644 index 000000000..28afb20bc --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Header.kt @@ -0,0 +1,12 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance.variants + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.HeaderAppearance +import com.atls.hyperion.ui.theme.tokens.colors.Colors as TokenColors + +@Composable +fun HeaderAppearance.Companion.default(): HeaderAppearance = + HeaderAppearance( + textColor = TokenColors.Text.black, + arrowColor = TokenColors.Text.black + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Picker.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Picker.kt new file mode 100644 index 000000000..12282c766 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/Picker.kt @@ -0,0 +1,24 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance.variants + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.components.divider.style.appearance.DividerAppearance +import com.atls.hyperion.ui.components.divider.style.appearance.default +import com.atls.hyperion.ui.components.modal.style.appearance.ModalAppearance +import com.atls.hyperion.ui.components.modal.style.appearance.default +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.CellAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.HeaderAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.WeekDaysAppearance +import com.atls.hyperion.ui.theme.tokens.colors.Colors as TokenColors + +@Composable +fun DatePickerAppearance.Companion.default(): DatePickerAppearance = + DatePickerAppearance( + cellAppearance = CellAppearance.default(), + headerAppearance = HeaderAppearance.default(), + weekDaysAppearance = WeekDaysAppearance.default(), + backgroundColor = TokenColors.Palette.white, + dividerColor = TokenColors.Palette.gray, + dividerAppearance = DividerAppearance.default(), + modalAppearance = ModalAppearance.default() + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/WeekDays.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/WeekDays.kt new file mode 100644 index 000000000..61a3199c8 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/appearance/variants/WeekDays.kt @@ -0,0 +1,11 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.appearance.variants + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.WeekDaysAppearance +import com.atls.hyperion.ui.theme.tokens.colors.Colors as TokenColors + +@Composable +fun WeekDaysAppearance.Companion.default(): WeekDaysAppearance = + WeekDaysAppearance( + textColor = TokenColors.Text.black + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Calendar.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Calendar.kt new file mode 100644 index 000000000..5644885c7 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Calendar.kt @@ -0,0 +1,15 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape + +import androidx.compose.foundation.layout.PaddingValues +import com.atls.hyperion.ui.components.divider.style.shape.DividerShape +import com.atls.hyperion.ui.components.modal.style.shape.ModalShape +import com.atls.hyperion.ui.fragment.datepicker.model.DividerPosition + +data class CalendarShape( + val padding: PaddingValues, + val divider: DividerPosition = DividerPosition.NONE, + val dividerShape: DividerShape, + val modalShape: ModalShape +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Cell.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Cell.kt new file mode 100644 index 000000000..1ff8bd822 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Cell.kt @@ -0,0 +1,22 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import com.atls.hyperion.ui.theme.tokens.layout.BorderStroke + +data class CellShape( + val spacing: Dp, + val padding: Dp, + val borderWidth: Dp = BorderStroke.none, + val borderColor: Color = Color.Transparent, + val shape: Shape, + val activeShape: Shape = shape, + val rangeShape: Shape = RectangleShape, + val typography: TextStyle, + val headerTypography: TextStyle +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Header.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Header.kt new file mode 100644 index 000000000..8f3d4ad94 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Header.kt @@ -0,0 +1,13 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp + +data class HeaderShape( + val typography: TextStyle, + val spacing: Dp, + val iconSize: Dp, + val horizontalPadding: Dp +) { + companion object +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Shape.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Shape.kt new file mode 100644 index 000000000..b552e3a92 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/Shape.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape + +data class DatePickerShape( + val cellShape: CellShape, + val headerShape: HeaderShape, + val calendarShape: CalendarShape +) { + companion object Companion +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Calendar.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Calendar.kt new file mode 100644 index 000000000..9c0f0fc14 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Calendar.kt @@ -0,0 +1,20 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape.variants + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.components.divider.style.shape.DividerShape +import com.atls.hyperion.ui.components.divider.style.shape.default +import com.atls.hyperion.ui.components.modal.style.shape.ModalShape +import com.atls.hyperion.ui.components.modal.style.shape.popup +import com.atls.hyperion.ui.fragment.datepicker.model.DividerPosition +import com.atls.hyperion.ui.fragment.datepicker.style.shape.CalendarShape +import com.atls.hyperion.ui.theme.tokens.layout.Space + +@Composable +fun CalendarShape.Companion.default(): CalendarShape = + CalendarShape( + padding = PaddingValues(Space.g12), + divider = DividerPosition.BOTTOM, + dividerShape = DividerShape.default(), + modalShape = ModalShape.popup() + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Cell.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Cell.kt new file mode 100644 index 000000000..9488b92a8 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Cell.kt @@ -0,0 +1,26 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape.variants + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import com.atls.hyperion.ui.fragment.datepicker.style.shape.CellShape +import com.atls.hyperion.ui.theme.tokens.layout.CornerRadius +import com.atls.hyperion.ui.theme.tokens.layout.Space +import com.atls.hyperion.ui.theme.typography.FontSize +import com.atls.hyperion.ui.theme.typography.FontWeight + +@Composable +fun CellShape.Companion.default(): CellShape = + CellShape( + spacing = Space.zero, + padding = Space.g8, + shape = RoundedCornerShape(CornerRadius.xs2), + typography = TextStyle( + fontSize = FontSize.xs, + fontWeight = FontWeight.regular, + ), + headerTypography = TextStyle( + fontSize = FontSize.xs2, + fontWeight = FontWeight.medium, + ) + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Header.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Header.kt new file mode 100644 index 000000000..f95c7a479 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Header.kt @@ -0,0 +1,21 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape.variants + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import com.atls.hyperion.ui.fragment.datepicker.style.shape.HeaderShape +import com.atls.hyperion.ui.primitives.icon.IconSize +import com.atls.hyperion.ui.theme.tokens.layout.Space +import com.atls.hyperion.ui.theme.typography.FontSize +import com.atls.hyperion.ui.theme.typography.FontWeight + +@Composable +fun HeaderShape.Companion.default(): HeaderShape = + HeaderShape( + typography = TextStyle( + fontSize = FontSize.md, + fontWeight = FontWeight.bold, + ), + spacing = Space.g12, + iconSize = IconSize.medium, + horizontalPadding = Space.g12 + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Picker.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Picker.kt new file mode 100644 index 000000000..5746a0e12 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/style/shape/variants/Picker.kt @@ -0,0 +1,15 @@ +package com.atls.hyperion.ui.fragment.datepicker.style.shape.variants + +import androidx.compose.runtime.Composable +import com.atls.hyperion.ui.fragment.datepicker.style.shape.CalendarShape +import com.atls.hyperion.ui.fragment.datepicker.style.shape.CellShape +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.fragment.datepicker.style.shape.HeaderShape + +@Composable +fun DatePickerShape.Companion.default(): DatePickerShape = + DatePickerShape( + cellShape = CellShape.default(), + headerShape = HeaderShape.default(), + calendarShape = CalendarShape.default() + ) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/CalendarHeader.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/CalendarHeader.kt new file mode 100644 index 000000000..1d82b3a63 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/CalendarHeader.kt @@ -0,0 +1,79 @@ +package com.atls.hyperion.ui.fragment.datepicker.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import com.atls.hyperion.ui.components.divider.horizontal.HorizontalDivider +import com.atls.hyperion.ui.fragment.datepicker.model.DividerPosition +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.generated.resources.Res +import com.atls.hyperion.ui.generated.resources.chevron_left +import com.atls.hyperion.ui.generated.resources.chevron_right +import com.atls.hyperion.ui.primitives.Text +import com.atls.hyperion.ui.primitives.VerticalSpacer +import com.atls.hyperion.ui.primitives.icon.Icon +import org.jetbrains.compose.resources.painterResource + +@Composable +fun CalendarHeader( + monthName: String, + appearance: DatePickerAppearance, + shape: DatePickerShape, + beforeIcon: Painter = painterResource(Res.drawable.chevron_left), + afterIcon: Painter = painterResource(Res.drawable.chevron_right), + arrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, + alignment: Alignment.Vertical = Alignment.CenterVertically, + onPrevMonth: () -> Unit, + onNextMonth: () -> Unit, +) { + if (shape.calendarShape.divider == DividerPosition.TOP) { + HorizontalDivider( + appearance = appearance.dividerAppearance, + shape = shape.calendarShape.dividerShape + ) + VerticalSpacer(shape.headerShape.spacing) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = shape.headerShape.horizontalPadding), + horizontalArrangement = arrangement, + verticalAlignment = alignment + ) { + Icon( + icon = beforeIcon, + size = shape.headerShape.iconSize, + modifier = Modifier.clickable { onPrevMonth() }, + color = appearance.headerAppearance.arrowColor + ) + + Text( + text = monthName, + typography = shape.headerShape.typography, + color = appearance.headerAppearance.textColor + ) + + Icon( + icon = afterIcon, + size = shape.headerShape.iconSize, + modifier = Modifier.clickable { onNextMonth() }, + color = appearance.headerAppearance.arrowColor + ) + } + + if (shape.calendarShape.divider == DividerPosition.BOTTOM) { + VerticalSpacer(shape.headerShape.spacing) + HorizontalDivider( + appearance = appearance.dividerAppearance, + shape = shape.calendarShape.dividerShape + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/DatePickerContent.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/DatePickerContent.kt new file mode 100644 index 000000000..3e010b20c --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/DatePickerContent.kt @@ -0,0 +1,149 @@ +package com.atls.hyperion.ui.fragment.datepicker.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.fragment.datepicker.config.DAYS_IN_WEEK +import com.atls.hyperion.ui.fragment.datepicker.config.WEEK_RANGE +import com.atls.hyperion.ui.fragment.datepicker.lib.next +import com.atls.hyperion.ui.fragment.datepicker.lib.previous +import com.atls.hyperion.ui.fragment.datepicker.model.DateSelection +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.primitives.layout.column.Column +import com.kizitonwose.calendar.compose.HorizontalCalendar +import com.kizitonwose.calendar.compose.rememberCalendarState +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import kotlinx.coroutines.launch +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.TimeZone +import kotlinx.datetime.YearMonth +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +@Composable +fun DatePickerContent( + modifier: Modifier = Modifier, + selection: DateSelection, + onSelectionChange: (DateSelection) -> Unit, + appearance: DatePickerAppearance, + shape: DatePickerShape, +) { + val today = remember { + Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + } + + val currentMonth = remember { YearMonth(today.year, today.month) } + val startMonth = remember { YearMonth(1, today.month) } + val endMonth = remember { YearMonth(2200, today.month) } + val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } + + val state = rememberCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, + firstDayOfWeek = firstDayOfWeek + ) + + val coroutineScope = rememberCoroutineScope() + + Column(modifier = modifier.background(appearance.backgroundColor)) { + + val visibleMonth = state.firstVisibleMonth.yearMonth + val monthName = + visibleMonth.month.name.lowercase().replaceFirstChar { it.uppercase() } + + CalendarHeader( + monthName = "$monthName ${visibleMonth.year}", + appearance = appearance, + shape = shape, + onPrevMonth = { + coroutineScope.launch { + state.animateScrollToMonth(visibleMonth.previous()) + } + }, + onNextMonth = { + coroutineScope.launch { + state.animateScrollToMonth(visibleMonth.next()) + } + } + ) + + HorizontalCalendar( + modifier = Modifier + .fillMaxWidth() + .padding(shape.calendarShape.padding), + + state = state, + dayContent = { day -> + val date = day.date + + val isSelected = when (selection) { + is DateSelection.Single -> selection.date == date + is DateSelection.Range -> false + } + + val isRangeStart = when (selection) { + is DateSelection.Range -> selection.start == date + else -> false + } + + val isRangeEnd = when (selection) { + is DateSelection.Range -> selection.end == date + else -> false + } + + val isInRange = when (selection) { + is DateSelection.Range -> + selection.start != null && + selection.end != null && + date > selection.start && + date < selection.end + + else -> false + } + + Day( + day = day, + isSelected = isSelected, + isInRange = isInRange, + isRangeStart = isRangeStart, + isRangeEnd = isRangeEnd, + appearance = appearance, + shape = shape, + onClick = { clicked -> + onSelectionChange( + when (selection) { + is DateSelection.Single -> + selection.copy(date = clicked) + + is DateSelection.Range -> + selection.next(clicked) + } + ) + } + ) + }, + + monthHeader = { + WeekDays( + weekDays = state.firstDayOfWeek.let { first -> + WEEK_RANGE.map { + DayOfWeek.entries[(first.ordinal + it) % DAYS_IN_WEEK] + } + }, + appearance = appearance, + shape = shape + ) + } + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/Day.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/Day.kt new file mode 100644 index 000000000..72a8237cd --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/Day.kt @@ -0,0 +1,99 @@ +package com.atls.hyperion.ui.fragment.datepicker.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.primitives.Text +import com.atls.hyperion.ui.shared.layout.aspectSquare +import com.atls.hyperion.ui.theme.tokens.colors.Colors +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.DayPosition +import kotlinx.datetime.LocalDate + +@Composable +fun Day( + day: CalendarDay, + isSelected: Boolean, + isInRange: Boolean, + isRangeStart: Boolean, + isRangeEnd: Boolean, + appearance: DatePickerAppearance, + shape: DatePickerShape, + onClick: (LocalDate) -> Unit, +) { + val isCurrentMonth = day.position == DayPosition.MonthDate + if (!isCurrentMonth) { + Box(modifier = Modifier.aspectSquare()) + return + } + + val date = LocalDate(day.date.year, day.date.monthNumber, day.date.dayOfMonth) + + val backgroundColor = when { + isSelected || isRangeStart || isRangeEnd -> appearance.cellAppearance.activeBackgroundColor + isInRange -> appearance.cellAppearance.inRangeBackgroundColor + else -> appearance.cellAppearance.backgroundColor + } + + val cellShape = when { + isSelected -> shape.cellShape.activeShape + isInRange -> shape.cellShape.rangeShape + else -> shape.cellShape.shape + } + + val textColor = when { + isSelected || isRangeStart || isRangeEnd -> appearance.cellAppearance.activeTextColor + isInRange -> appearance.cellAppearance.inRangeTextColor + else -> appearance.cellAppearance.textColor + } + + + val containerModifier = Modifier + .aspectSquare() + .padding(shape.cellShape.spacing) + .clip(cellShape) + .background(backgroundColor) + .padding(shape.cellShape.padding) + .then( + if ((isSelected || isRangeStart || isRangeEnd) && appearance.cellAppearance.activeBorderColor != Colors.Palette.transparent) + Modifier.border( + shape.cellShape.borderWidth, + appearance.cellAppearance.activeBorderColor, + cellShape + ) + else if (isInRange && appearance.cellAppearance.inRangeBorderColor != Colors.Palette.transparent) + Modifier.border( + shape.cellShape.borderWidth, + appearance.cellAppearance.inRangeBorderColor, + cellShape + ) + else if (appearance.cellAppearance.borderColor != Colors.Palette.transparent) + Modifier.border( + shape.cellShape.borderWidth, + appearance.cellAppearance.borderColor, + cellShape + ) + else Modifier + ) + .clickable { onClick(date) } + + + Box( + modifier = containerModifier, + contentAlignment = Alignment.Center + ) { + Text( + text = day.date.dayOfMonth.toString(), + color = textColor, + typography = shape.cellShape.typography + ) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/WeekDays.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/WeekDays.kt new file mode 100644 index 000000000..d4a355a18 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/fragment/datepicker/ui/WeekDays.kt @@ -0,0 +1,39 @@ +package com.atls.hyperion.ui.fragment.datepicker.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import com.atls.hyperion.ui.fragment.datepicker.config.DAYS_IN_WEEK +import com.atls.hyperion.ui.fragment.datepicker.style.appearance.DatePickerAppearance +import com.atls.hyperion.ui.fragment.datepicker.style.shape.DatePickerShape +import com.atls.hyperion.ui.primitives.Text +import com.atls.hyperion.ui.theme.tokens.layout.Weight +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.format.DayOfWeekNames + +@Composable +fun WeekDays( + weekDays: List, + appearance: DatePickerAppearance, + shape: DatePickerShape, + dayNames: List = DayOfWeekNames.ENGLISH_ABBREVIATED.names +) { + require(dayNames.size == DAYS_IN_WEEK) { "dayNames must contain exactly 7 elements (Mon → Sun)" } + + Row(modifier = Modifier.fillMaxWidth()) { + weekDays.forEach { dayOfWeek -> + val nameIndex = (dayOfWeek.ordinal - weekDays.first().ordinal + DAYS_IN_WEEK) % DAYS_IN_WEEK + val shortName = dayNames[nameIndex] + + Text( + modifier = Modifier.weight(Weight.full), + textAlign = TextAlign.Center, + text = shortName, + typography = shape.cellShape.headerTypography, + color = appearance.weekDaysAppearance.textColor, + ) + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Icon.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Primitive.kt similarity index 92% rename from mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Icon.kt rename to mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Primitive.kt index 9aafb876d..2f9897700 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Icon.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Primitive.kt @@ -1,4 +1,4 @@ -package com.atls.hyperion.ui.primitives +package com.atls.hyperion.ui.primitives.icon import androidx.compose.foundation.layout.size import androidx.compose.material.Icon diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Size.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Size.kt new file mode 100644 index 000000000..8e6faec47 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/icon/Size.kt @@ -0,0 +1,7 @@ +package com.atls.hyperion.ui.primitives.icon + +import androidx.compose.ui.unit.dp + +object IconSize { + val medium = 24.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Lazy.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Lazy.kt new file mode 100644 index 000000000..e5ac8dd0c --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Lazy.kt @@ -0,0 +1,39 @@ +package com.atls.hyperion.ui.primitives.layout.column + +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.theme.tokens.layout.Space +import androidx.compose.foundation.lazy.LazyColumn as ComposeColumn + +@Composable +fun LazyColumn( + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(Space.zero), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + content: LazyListScope.() -> Unit +) { + ComposeColumn( + modifier = modifier, + state = state, + contentPadding = contentPadding, + reverseLayout = reverseLayout, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + flingBehavior = flingBehavior, + userScrollEnabled = userScrollEnabled, + content = content + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Static.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Static.kt new file mode 100644 index 000000000..8a76a92d6 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/column/Static.kt @@ -0,0 +1,23 @@ +package com.atls.hyperion.ui.primitives.layout.column + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.foundation.layout.Column as ComposeColumn + +@Composable +fun Column( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable ColumnScope.() -> Unit +) { + ComposeColumn( + modifier = modifier, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + content = content + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Lazy.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Lazy.kt new file mode 100644 index 000000000..aa828d3b0 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Lazy.kt @@ -0,0 +1,42 @@ +package com.atls.hyperion.ui.primitives.layout.grid + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp + +@Composable +fun LazyGrid( + columns: Int, + orientation: GridOrientation, + modifier: Modifier = Modifier, + horizontalSpacing: Dp, + verticalSpacing: Dp, + content: LazyGridScope.() -> Unit +) { + when (orientation) { + GridOrientation.Vertical -> { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + content = content + ) + } + + GridOrientation.Horizontal -> { + LazyHorizontalGrid( + rows = GridCells.Fixed(columns), + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + content = content + ) + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Orientation.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Orientation.kt new file mode 100644 index 000000000..78e05afb1 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Orientation.kt @@ -0,0 +1,6 @@ +package com.atls.hyperion.ui.primitives.layout.grid + +enum class GridOrientation { + Vertical, + Horizontal +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Static.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Static.kt new file mode 100644 index 000000000..4d2558604 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/grid/Static.kt @@ -0,0 +1,70 @@ +package com.atls.hyperion.ui.primitives.layout.grid + +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.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.atls.hyperion.ui.primitives.VerticalSpacer +import com.atls.hyperion.ui.theme.tokens.layout.Weight + +@Composable +fun Grid( + items: List<@Composable () -> Unit>, + columns: Int, + orientation: GridOrientation, + modifier: Modifier = Modifier, + horizontalSpacing: Dp, + verticalSpacing: Dp +) { + when (orientation) { + GridOrientation.Vertical -> { + Column(modifier = modifier) { + items.chunked(columns).forEach { rowItems -> + Row( + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + modifier = Modifier.fillMaxWidth() + ) { + rowItems.forEach { item -> + Box(modifier = Modifier.weight(Weight.full)) { + item() + } + } + + if (rowItems.size < columns) { + repeat(columns - rowItems.size) { + Spacer(modifier = Modifier.weight(Weight.full)) + } + } + } + VerticalSpacer(verticalSpacing) + } + } + } + + GridOrientation.Horizontal -> { + Row(modifier = modifier) { + items.chunked(columns).forEach { columnItems -> + Column( + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + modifier = Modifier.weight(Weight.full) + ) { + columnItems.forEach { item -> + item() + } + + if (columnItems.size < columns) { + repeat(columns - columnItems.size) { + Spacer(modifier = Modifier.weight(Weight.full)) + } + } + } + } + } + } + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Lazy.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Lazy.kt new file mode 100644 index 000000000..dea948061 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Lazy.kt @@ -0,0 +1,39 @@ +package com.atls.hyperion.ui.primitives.layout.row + +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.atls.hyperion.ui.theme.tokens.layout.Space +import androidx.compose.foundation.lazy.LazyRow as ComposeRow + +@Composable +fun LazyRow( + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(Space.zero), + reverseLayout: Boolean = false, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + content: LazyListScope.() -> Unit +) { + ComposeRow( + modifier = modifier, + state = state, + contentPadding = contentPadding, + reverseLayout = reverseLayout, + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + flingBehavior = flingBehavior, + userScrollEnabled = userScrollEnabled, + content = content + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Static.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Static.kt new file mode 100644 index 000000000..a7cf0542c --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/layout/row/Static.kt @@ -0,0 +1,23 @@ +package com.atls.hyperion.ui.primitives.layout.row + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.foundation.layout.Row as ComposeRow + +@Composable +fun Row( + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: @Composable RowScope.() -> Unit +) { + ComposeRow( + modifier = modifier, + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + content = content + ) +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/shared/layout/AspectSquare.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/shared/layout/AspectSquare.kt new file mode 100644 index 000000000..50377e1f5 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/shared/layout/AspectSquare.kt @@ -0,0 +1,9 @@ +package com.atls.hyperion.ui.shared.layout + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun Modifier.aspectSquare(): Modifier = + this.aspectRatio(1f) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/effects/Alpha.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/effects/Alpha.kt new file mode 100644 index 000000000..6ef296fef --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/effects/Alpha.kt @@ -0,0 +1,7 @@ +package com.atls.hyperion.ui.theme.tokens.effects + +object Alpha { + val huge = 0.9f + val large = 0.8f + val medium = 0.6f +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/Elevation.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt similarity index 53% rename from mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/Elevation.kt rename to mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt index ff5674e06..29d13b027 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/Elevation.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt @@ -1,7 +1,8 @@ -package com.atls.hyperion.ui.theme.tokens +package com.atls.hyperion.ui.theme.tokens.layout import androidx.compose.ui.unit.dp object Elevation { val zero = 0.dp + val tiny = 2.dp }