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 ba5bba67d..c7ee465e1 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 @@ -25,6 +25,7 @@ import com.atls.hyperion.ui.fragments.datepicker.stories.DateRangePickerStory import com.atls.hyperion.ui.primitives.stories.LinkStory import com.atls.hyperion.ui.primitives.stories.TextStory import com.atls.hyperion.ui.theme.Theme +import com.atls.hyperion.ui.theme.tokens.elevation.stories.ElevationStory @Composable fun App() { @@ -41,6 +42,7 @@ fun App() { DatePickerStory(), DateRangePickerStory(), DividerStory(), + ElevationStory(), InputStory(), PaginationStory(), PlaceholderStory(), diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Elevation.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Elevation.kt new file mode 100644 index 000000000..d2614d89b --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/primitives/Elevation.kt @@ -0,0 +1,50 @@ +package com.atls.hyperion.ui.primitives + +import androidx.compose.foundation.background +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.unit.DpOffset +import com.atls.hyperion.ui.theme.tokens.elevation.Elevation +import com.atls.hyperion.ui.theme.tokens.elevation.ShadowType + +fun Modifier.elevation( + elevation: Elevation, + backgroundColor: Color, + shape: Shape = RectangleShape +): Modifier { + val withDropShadows = elevation.shadows.fold(this) { modifier, shadow -> + if (shadow.type == ShadowType.Drop) { + modifier.dropShadow( + shape = shape, + shadow = shadow.toComposeShadow() + ) + } else { + modifier + } + } + + val withBackground = withDropShadows.background(backgroundColor, shape) + + return elevation.shadows.fold(withBackground) { modifier, shadow -> + if (shadow.type == ShadowType.Inner) { + modifier.innerShadow( + shape = shape, + shadow = shadow.toComposeShadow() + ) + } else { + modifier + } + } +} + +private fun com.atls.hyperion.ui.theme.tokens.elevation.Shadow.toComposeShadow() = Shadow( + radius = blur, + spread = spread, + offset = DpOffset(offsetX, offsetY), + color = color +) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/Theme.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/Theme.kt index 5ab861f1b..aeb5e7b34 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/Theme.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/Theme.kt @@ -8,11 +8,13 @@ import com.atls.hyperion.ui.theme.tokens.borders.Borders import com.atls.hyperion.ui.theme.tokens.colors.Colors import com.atls.hyperion.ui.theme.tokens.colors.darkColors import com.atls.hyperion.ui.theme.tokens.colors.lightColors +import com.atls.hyperion.ui.theme.tokens.elevation.Elevations import com.atls.hyperion.ui.theme.typography.TextStyles import com.atls.hyperion.ui.theme.typography.fontFamilies.ScienceGothicFontFamily val LocalHyperionColors = staticCompositionLocalOf { lightColors } val LocalHyperionBorders = staticCompositionLocalOf { Borders(lightColors) } +val LocalHyperionElevations = staticCompositionLocalOf { Elevations(lightColors) } val LocalHyperionTypography = staticCompositionLocalOf { TextStyles(fontFamily = FontFamily.Default) } @Composable @@ -22,11 +24,13 @@ fun Theme( content: @Composable () -> Unit ) { val borders = Borders(colors) + val elevations = Elevations(colors) val typography = TextStyles(fontFamily = ScienceGothicFontFamily()) CompositionLocalProvider( LocalHyperionColors provides colors, LocalHyperionBorders provides borders, + LocalHyperionElevations provides elevations, LocalHyperionTypography provides typography ) { content() diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Dark.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Dark.kt index c52130fc9..f648fc8ba 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Dark.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Dark.kt @@ -8,7 +8,7 @@ val darkColors = Colors( hover = Color(0xFF7AA7FF), pressed = Color(0xFF3F74FF), disabled = Color(0xFF5F8FFF), - subtle = Color(0xFF394A6B) + subtle = Color(0x4D5F8FFF) ), surface = SurfaceColors( base = Color(0xFF111111), diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Light.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Light.kt index 8e4212513..f9762fa6f 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Light.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/colors/Light.kt @@ -8,7 +8,7 @@ val lightColors = Colors( hover = Color(0xFF1643A3), pressed = Color(0xFF12357F), disabled = Color(0x401E56C7), - subtle = Color(0xFF6B86C8) + subtle = Color(0x401E56C7) ), surface = SurfaceColors( base = Color(0xFFF8F9FF), diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Blurs.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Blurs.kt new file mode 100644 index 000000000..385f1e0f2 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Blurs.kt @@ -0,0 +1,17 @@ +package com.atls.hyperion.ui.theme.tokens.elevation + +import androidx.compose.ui.unit.dp + +object ShadowBlur { + val xs5 = 1.dp + val xs4 = 2.dp + val xs3 = 3.dp + val xs2 = 4.dp + val xs = 5.dp + val sm = 6.dp + val md = 8.dp + val lg = 12.dp + val xl = 14.dp + val xl2 = 16.dp + val xl3 = 20.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Elevations.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Elevations.kt new file mode 100644 index 000000000..4bb08e5d7 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Elevations.kt @@ -0,0 +1,136 @@ +package com.atls.hyperion.ui.theme.tokens.elevation + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.atls.hyperion.ui.theme.tokens.colors.Colors + +class Elevations(colors: Colors) { + val xs: PressableElevationStates = Xs(colors.elevation.xs) + val sm: PressableElevationStates = Sm(colors.elevation.sm) + val md: PressableElevationStates = Md(colors.elevation.md) + val lg: PressableElevationStates = Lg(colors.elevation.lg) + val modal: ElevationStates = Modal(colors.elevation.md) + + private class Xs(color: Color) : PressableElevationStates { + override val default = elevation( + drop(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs4) + ) + override val pressed = elevation( + inner(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs5), + drop( + color, + offsetY = ShadowOffset.xs, + blur = ShadowBlur.xs5, + spread = ShadowSpread.negativeXs + ) + ) + override val disabled = elevation( + drop(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs5) + ) + } + + private class Sm(color: Color) : PressableElevationStates { + override val default = elevation( + drop(color, offsetY = ShadowOffset.sm, blur = ShadowBlur.sm) + ) + override val pressed = elevation( + inner(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs4), + drop( + color, + offsetY = ShadowOffset.xs, + blur = ShadowBlur.xs3, + spread = ShadowSpread.negativeSm + ) + ) + override val disabled = elevation( + drop(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs4) + ) + } + + private class Md(color: Color) : PressableElevationStates { + override val default = elevation( + drop(color, offsetY = ShadowOffset.lg, blur = ShadowBlur.lg) + ) + override val pressed = elevation( + inner(color, offsetY = ShadowOffset.xs, blur = ShadowBlur.xs3), + drop( + color, + offsetY = ShadowOffset.sm, + blur = ShadowBlur.sm, + spread = ShadowSpread.negativeXs + ) + ) + override val disabled = elevation( + drop(color, offsetY = ShadowOffset.sm, blur = ShadowBlur.xs) + ) + } + + private class Lg(color: Color) : PressableElevationStates { + override val default = elevation( + drop( + color, + offsetY = ShadowOffset.xl, + blur = ShadowBlur.lg, + spread = ShadowSpread.negativeLg + ) + ) + override val pressed = elevation( + inner(color, offsetY = ShadowOffset.sm, blur = ShadowBlur.xs2), + drop( + color, + offsetY = ShadowOffset.md, + blur = ShadowBlur.md, + spread = ShadowSpread.negativeLg + ) + ) + override val disabled = elevation( + drop( + color, + offsetY = ShadowOffset.md, + blur = ShadowBlur.sm, + spread = ShadowSpread.negativeMd + ) + ) + } + + private class Modal(color: Color) : ElevationStates { + override val default = elevation( + drop( + color, + offsetY = ShadowOffset.xl3, + blur = ShadowBlur.xl3, + spread = ShadowSpread.negativeSm + ) + ) + } +} + +private fun elevation(vararg shadows: Shadow) = Elevation(shadows.toList()) + +private fun drop( + color: Color, + offsetX: Dp = ShadowOffset.none, + offsetY: Dp, + blur: Dp, + spread: Dp = ShadowSpread.none +) = Shadow( + type = ShadowType.Drop, + offsetX = offsetX, + offsetY = offsetY, + blur = blur, + spread = spread, + color = color +) + +private fun inner( + color: Color, + offsetY: Dp, + blur: Dp +) = Shadow( + type = ShadowType.Inner, + offsetX = ShadowOffset.none, + offsetY = offsetY, + blur = blur, + spread = ShadowSpread.none, + color = color +) diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Interfaces.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Interfaces.kt new file mode 100644 index 000000000..ef26b1582 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Interfaces.kt @@ -0,0 +1,33 @@ +package com.atls.hyperion.ui.theme.tokens.elevation + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +data class Elevation( + val shadows: List +) + +@Immutable +data class Shadow( + val type: ShadowType, + val offsetX: Dp, + val offsetY: Dp, + val blur: Dp, + val spread: Dp, + val color: Color +) + +enum class ShadowType { + Drop, + Inner +} + +interface ElevationStates { + val default: Elevation +} + +interface PressableElevationStates : ElevationStates { + val pressed: Elevation + val disabled: Elevation +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Offsets.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Offsets.kt new file mode 100644 index 000000000..ddd47a368 --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Offsets.kt @@ -0,0 +1,14 @@ +package com.atls.hyperion.ui.theme.tokens.elevation + +import androidx.compose.ui.unit.dp + +object ShadowOffset { + val none = 0.dp + val xs = 1.dp + val sm = 2.dp + val md = 3.dp + val lg = 4.dp + val xl = 6.dp + val xl2 = 7.dp + val xl3 = 12.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Spreads.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Spreads.kt new file mode 100644 index 000000000..283df4b0b --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/Spreads.kt @@ -0,0 +1,12 @@ +package com.atls.hyperion.ui.theme.tokens.elevation + +import androidx.compose.ui.unit.dp + +object ShadowSpread { + val negativeLg = (-4).dp + val negativeMd = (-3).dp + val negativeSm = (-2).dp + val negativeXs = (-1).dp + val none = 0.dp + val xs = 1.dp +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/stories/Component.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/stories/Component.kt new file mode 100644 index 000000000..5b7befc8f --- /dev/null +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/elevation/stories/Component.kt @@ -0,0 +1,158 @@ +package com.atls.hyperion.ui.theme.tokens.elevation.stories + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import com.atls.hyperion.storybook.shared.model.ComponentExample +import com.atls.hyperion.ui.primitives.elevation +import com.atls.hyperion.ui.theme.LocalHyperionColors +import com.atls.hyperion.ui.theme.LocalHyperionElevations +import com.atls.hyperion.ui.theme.Theme +import com.atls.hyperion.ui.theme.tokens.elevation.Elevation +import com.atls.hyperion.ui.theme.tokens.elevation.PressableElevationStates +import com.atls.hyperion.ui.theme.tokens.layout.Radii +import com.atls.hyperion.ui.theme.tokens.layout.Spacing + +class ElevationStory : ComponentExample { + override val name: String = "Elevation" + + @Composable + override fun Content() { + var darkTheme by remember { mutableStateOf(false) } + var enabled by remember { mutableStateOf(true) } + + Theme(darkTheme = darkTheme) { + val colors = LocalHyperionColors.current + val elevations = LocalHyperionElevations.current + val pressableElevations = listOf( + "xs" to elevations.xs, + "sm" to elevations.sm, + "md" to elevations.md, + "lg" to elevations.lg + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(colors.surface.subtle) + .verticalScroll(rememberScrollState()) + .padding(Spacing.component.xl3), + verticalArrangement = Arrangement.spacedBy(Spacing.component.xl3) + ) { + Toggle( + label = "Dark theme", + checked = darkTheme, + textColor = colors.text.primary, + onCheckedChange = { darkTheme = it } + ) + Toggle( + label = "Enabled", + checked = enabled, + textColor = colors.text.primary, + onCheckedChange = { enabled = it } + ) + + pressableElevations.forEach { (label, states) -> + ElevationCard( + label = label, + states = states, + enabled = enabled, + backgroundColor = colors.surface.base, + textColor = colors.text.primary + ) + } + + StaticElevationCard( + label = "modal · default", + elevation = elevations.modal.default, + backgroundColor = colors.surface.base, + textColor = colors.text.primary + ) + } + } + } +} + +@Composable +private fun Toggle( + label: String, + checked: Boolean, + textColor: Color, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(label, color = textColor) + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} + +@Composable +private fun ElevationCard( + label: String, + states: PressableElevationStates, + enabled: Boolean, + backgroundColor: Color, + textColor: Color +) { + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val state = when { + !enabled -> "disabled" to states.disabled + pressed -> "pressed" to states.pressed + else -> "default" to states.default + } + + StaticElevationCard( + label = "$label · ${state.first}", + elevation = state.second, + backgroundColor = backgroundColor, + textColor = textColor, + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + enabled = enabled, + onClick = {} + ) + ) +} + +@Composable +private fun StaticElevationCard( + label: String, + elevation: Elevation, + backgroundColor: Color, + textColor: Color, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(Radii.md) +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(Spacing.layout.lg) + .elevation( + elevation = elevation, + backgroundColor = backgroundColor, + shape = shape + ) + .then(modifier), + contentAlignment = Alignment.Center + ) { + Text(label, color = textColor) + } +} diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt index f779aa15c..76b7d991b 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/layout/Elevation.kt @@ -2,7 +2,7 @@ package com.atls.hyperion.ui.theme.tokens.layout import androidx.compose.ui.unit.dp -object Elevation { +object Elevation { //TODO remove after components refactoring val zero = 0.dp val tiny = 2.dp val medium = 8.dp diff --git a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/shadows/Shadows.kt b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/shadows/Shadows.kt index fb3f3cb6c..8040fb900 100644 --- a/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/shadows/Shadows.kt +++ b/mobile/kmp/ui/src/commonMain/kotlin/com/atls/hyperion/ui/theme/tokens/shadows/Shadows.kt @@ -3,7 +3,7 @@ package com.atls.hyperion.ui.theme.tokens.shadows import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -object Shadows { +object Shadows { //TODO remove after components refactoring val gunsmoke = Shadow(0.dp, 0.dp, 1.dp, Color(0x1A878787)) val gray = Shadow(0.dp, 1.dp, 1.dp, Color(0x17878787)) val grey = Shadow((-1).dp, 2.dp, 2.dp, Color(0x0D878787))