diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChip.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChip.kt new file mode 100644 index 0000000..fe721ec --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChip.kt @@ -0,0 +1,126 @@ +package com.team.prezel.core.designsystem.component.chip + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.team.prezel.core.designsystem.icon.DrawableIcon +import com.team.prezel.core.designsystem.icon.IconSource +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.preview.PreviewScaffold +import com.team.prezel.core.designsystem.preview.ThemePreview +import com.team.prezel.core.designsystem.theme.PrezelTheme + +@Composable +fun PrezelChip( + modifier: Modifier = Modifier, + text: String? = null, + icon: IconSource? = null, + style: PrezelChipStyle = PrezelChipStyle(), +) { + val hasText = text != null + val hasIcon = icon != null + val iconOnly = hasIcon && !hasText + require(hasText || hasIcon) { "Chip은 text 또는 icon 중 하나는 반드시 필요합니다." } + + Surface( + modifier = modifier, + shape = style.shape(), + color = style.containerColor(iconOnly = iconOnly), + border = style.borderStroke(), + ) { + CompositionLocalProvider( + LocalTextStyle provides style.textStyle(), + LocalContentColor provides style.contentColor(), + ) { + Row( + modifier = Modifier.padding(style.contentPadding(iconOnly = iconOnly)), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + PrezelChipIcon(icon = icon, style = style) + + if (hasText) { + if (hasIcon) { + Spacer(modifier = Modifier.width(style.iconTextSpacing())) + } + Text(text = text) + } + } + } + } +} + +@Composable +fun PrezelChip( + modifier: Modifier = Modifier, + text: String? = null, + icon: IconSource? = null, + style: PrezelChipStyle = PrezelChipStyle(), + customColors: PrezelChipColors = LocalPrezelChipColors.current, +) { + CompositionLocalProvider( + LocalPrezelChipColors provides customColors, + ) { + PrezelChip( + modifier = modifier, + text = text, + icon = icon, + style = style, + ) + } +} + +@Composable +private fun PrezelChipIcon( + icon: IconSource?, + style: PrezelChipStyle, + modifier: Modifier = Modifier, +) { + if (icon == null) return + + Icon( + painter = icon.painter(), + contentDescription = icon.contentDescription(), + modifier = modifier.size(style.iconSize()), + ) +} + +@ThemePreview +@Composable +private fun PrezelChipPreview() { + PrezelTheme { + PreviewScaffold { + PrezelChipPreviewByType( + type = PrezelChipType.FILLED, + ) { style -> PrezelChipPreviewItem(style) } + + HorizontalDivider() + + PrezelChipPreviewByType( + type = PrezelChipType.OUTLINED, + ) { style -> PrezelChipPreviewItem(style) } + } + } +} + +@Composable +private fun PrezelChipPreviewItem(style: PrezelChipStyle) { + PrezelChip( + text = "Label", + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = style, + ) +} diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipPreview.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipPreview.kt new file mode 100644 index 0000000..5c2a74d --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipPreview.kt @@ -0,0 +1,130 @@ +package com.team.prezel.core.designsystem.component.chip + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.team.prezel.core.designsystem.theme.PrezelTheme +import kotlinx.collections.immutable.persistentListOf + +internal typealias PrezelChipPreviewContent = @Composable (PrezelChipStyle) -> Unit + +private val DefaultInteractionVariants = persistentListOf( + PrezelChipInteraction.DEFAULT, + PrezelChipInteraction.ACTIVE, + PrezelChipInteraction.DISABLED, +) + +private val PreviewSizes = persistentListOf( + PrezelChipSize.SMALL, + PrezelChipSize.REGULAR, +) + +@Composable +internal fun PrezelChipPreviewByType( + type: PrezelChipType, + content: PrezelChipPreviewContent, +) { + Text( + text = type.name, + style = PrezelTheme.typography.title2Medium, + ) + + HorizontalDivider() + + PrezelChipBadSection( + type = type, + content = content, + ) + + HorizontalDivider() + + PrezelChipDefaultSection( + type = type, + content = content, + ) +} + +@Composable +private fun PrezelChipBadSection( + type: PrezelChipType, + content: PrezelChipPreviewContent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Feedback: BAD", + style = PrezelTheme.typography.body2Medium, + ) + + PrezelChipPreviewBlock( + type = type, + feedback = PrezelChipFeedback.BAD, + interaction = PrezelChipInteraction.DEFAULT, + content = content, + ) + } +} + +@Composable +private fun PrezelChipDefaultSection( + type: PrezelChipType, + content: PrezelChipPreviewContent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Feedback: DEFAULT", + style = PrezelTheme.typography.body2Medium, + ) + + DefaultInteractionVariants.forEach { interaction -> + Text( + text = "Interaction: $interaction", + style = PrezelTheme.typography.body3Medium, + ) + + PrezelChipPreviewBlock( + type = type, + feedback = PrezelChipFeedback.DEFAULT, + interaction = interaction, + content = content, + ) + } + } +} + +@Composable +private fun PrezelChipPreviewBlock( + type: PrezelChipType, + interaction: PrezelChipInteraction, + feedback: PrezelChipFeedback, + content: PrezelChipPreviewContent, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + PreviewSizes.forEach { size -> + content( + PrezelChipStyle( + type = type, + size = size, + interaction = interaction, + feedback = feedback, + ), + ) + } + } +} diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipStyle.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipStyle.kt new file mode 100644 index 0000000..2629ca5 --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelChipStyle.kt @@ -0,0 +1,168 @@ +package com.team.prezel.core.designsystem.component.chip + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.team.prezel.core.designsystem.foundation.color.PrezelColors +import com.team.prezel.core.designsystem.foundation.number.PrezelShapes +import com.team.prezel.core.designsystem.foundation.number.PrezelSpacing +import com.team.prezel.core.designsystem.foundation.number.PrezelStroke +import com.team.prezel.core.designsystem.foundation.typography.PrezelTypography +import com.team.prezel.core.designsystem.theme.PrezelTheme + +internal val LocalPrezelChipColors = staticCompositionLocalOf { PrezelChipColors() } + +enum class PrezelChipType { + FILLED, + OUTLINED, +} + +enum class PrezelChipInteraction { + DEFAULT, + ACTIVE, + DISABLED, +} + +enum class PrezelChipFeedback { + DEFAULT, + BAD, +} + +enum class PrezelChipSize { + SMALL, + REGULAR, +} + +@Immutable +data class PrezelChipColors( + val containerColor: Color = Color.Unspecified, + val contentColor: Color = Color.Unspecified, +) + +@Immutable +data class PrezelChipStyle( + val type: PrezelChipType = PrezelChipType.FILLED, + val size: PrezelChipSize = PrezelChipSize.REGULAR, + val interaction: PrezelChipInteraction = PrezelChipInteraction.DEFAULT, + val feedback: PrezelChipFeedback = PrezelChipFeedback.DEFAULT, +) { + @Composable + internal fun shape(shapes: PrezelShapes = PrezelTheme.shapes): Shape = + when (size) { + PrezelChipSize.SMALL -> shapes.V4 + PrezelChipSize.REGULAR -> shapes.V8 + } + + @Composable + internal fun borderStroke( + colors: PrezelColors = PrezelTheme.colors, + stroke: PrezelStroke = PrezelTheme.stroke, + chipColors: PrezelChipColors = LocalPrezelChipColors.current, + ): BorderStroke? { + if (type == PrezelChipType.FILLED) return null + + val borderColor = + when { + chipColors.contentColor != Color.Unspecified -> chipColors.contentColor + feedback == PrezelChipFeedback.BAD -> colors.feedbackBadRegular + interaction == PrezelChipInteraction.ACTIVE -> colors.interactiveRegular + interaction == PrezelChipInteraction.DISABLED -> colors.borderRegular + else -> colors.borderMedium + } + + return BorderStroke( + width = stroke.V1, + color = borderColor, + ) + } + + @Composable + internal fun textStyle(typography: PrezelTypography = PrezelTheme.typography): TextStyle = + when (size) { + PrezelChipSize.SMALL -> typography.caption2Regular + PrezelChipSize.REGULAR -> typography.caption1Regular + } + + @Composable + internal fun containerColor( + iconOnly: Boolean, + colors: PrezelColors = PrezelTheme.colors, + chipColors: PrezelChipColors = LocalPrezelChipColors.current, + ): Color { + if (type == PrezelChipType.OUTLINED && iconOnly) { + return Color.Transparent + } + + return when { + chipColors.containerColor != Color.Unspecified -> chipColors.containerColor + feedback == PrezelChipFeedback.BAD -> colors.feedbackBadSmall + interaction == PrezelChipInteraction.ACTIVE -> colors.interactiveXSmall + interaction == PrezelChipInteraction.DISABLED -> colors.bgLarge + else -> { + when (type) { + PrezelChipType.FILLED -> colors.bgMedium + PrezelChipType.OUTLINED -> colors.bgRegular + } + } + } + } + + @Composable + internal fun contentColor( + colors: PrezelColors = PrezelTheme.colors, + chipColors: PrezelChipColors = LocalPrezelChipColors.current, + ): Color = + when { + chipColors.contentColor != Color.Unspecified -> chipColors.contentColor + feedback == PrezelChipFeedback.BAD -> colors.feedbackBadRegular + interaction == PrezelChipInteraction.ACTIVE -> colors.interactiveRegular + interaction == PrezelChipInteraction.DISABLED -> colors.iconDisabled + else -> colors.iconRegular + } + + @Composable + internal fun contentPadding( + iconOnly: Boolean, + spacing: PrezelSpacing = PrezelTheme.spacing, + ): PaddingValues { + if (iconOnly) { + val all = when (size) { + PrezelChipSize.SMALL -> spacing.V6 + PrezelChipSize.REGULAR -> spacing.V8 + } + return PaddingValues(all = all) + } + + val horizontal = when (size) { + PrezelChipSize.SMALL -> spacing.V6 + PrezelChipSize.REGULAR -> spacing.V8 + } + + val vertical = when (size) { + PrezelChipSize.SMALL -> spacing.V4 + PrezelChipSize.REGULAR -> spacing.V6 + } + + return PaddingValues(horizontal = horizontal, vertical = vertical) + } + + @Composable + internal fun iconTextSpacing(spacing: PrezelSpacing = PrezelTheme.spacing): Dp = + when (size) { + PrezelChipSize.REGULAR -> spacing.V4 + PrezelChipSize.SMALL -> spacing.V2 + } + + internal fun iconSize(): Dp = + when (size) { + PrezelChipSize.SMALL -> 14.dp + PrezelChipSize.REGULAR -> 16.dp + } +} diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelCustomChipPreview.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelCustomChipPreview.kt new file mode 100644 index 0000000..5294f03 --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelCustomChipPreview.kt @@ -0,0 +1,176 @@ +package com.team.prezel.core.designsystem.component.chip + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.team.prezel.core.designsystem.icon.DrawableIcon +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.preview.PreviewScaffold +import com.team.prezel.core.designsystem.preview.ThemePreview +import com.team.prezel.core.designsystem.theme.PrezelTheme + +@ThemePreview +@Composable +private fun PrezelChip_CustomColors_Preview() { + PrezelTheme { + PreviewScaffold { + CustomChipHeader() + CustomChipLabelSection() + CustomChipIconOnlySection() + } + } +} + +@Composable +private fun CustomChipHeader() { + Text( + text = "Custom Chip", + style = PrezelTheme.typography.title2Medium, + ) +} + +@Composable +private fun CustomChipLabelSection() { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachRow = 3, + ) { + CustomYellowLabelChip() + CustomRedLabelChip() + CustomGreenLabelChip() + } +} + +@Composable +private fun CustomChipIconOnlySection() { + Text( + text = "Icon only", + style = PrezelTheme.typography.body3Medium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + CustomYellowIconChip() + CustomRedIconChip() + CustomGreenIconChip() + } +} + +@Composable +private fun CustomYellowLabelChip() { + PrezelChip( + text = "느려요", + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = PrezelChipStyle( + type = PrezelChipType.FILLED, + size = PrezelChipSize.REGULAR, + interaction = PrezelChipInteraction.DISABLED, + feedback = PrezelChipFeedback.BAD, + ), + customColors = PrezelChipColors( + containerColor = PrezelTheme.colors.feedbackWarningSmall, + contentColor = PrezelTheme.colors.feedbackWarningRegular, + ), + ) +} + +@Composable +private fun CustomRedLabelChip() { + PrezelChip( + text = "빨라요", + icon = null, + style = PrezelChipStyle( + type = PrezelChipType.OUTLINED, + size = PrezelChipSize.REGULAR, + interaction = PrezelChipInteraction.DEFAULT, + feedback = PrezelChipFeedback.DEFAULT, + ), + customColors = PrezelChipColors( + containerColor = PrezelTheme.colors.feedbackBadSmall, + contentColor = PrezelTheme.colors.feedbackBadRegular, + ), + ) +} + +@Composable +private fun CustomGreenLabelChip() { + PrezelChip( + text = "적당해요", + icon = null, + style = PrezelChipStyle( + type = PrezelChipType.FILLED, + size = PrezelChipSize.SMALL, + interaction = PrezelChipInteraction.ACTIVE, + feedback = PrezelChipFeedback.DEFAULT, + ), + customColors = PrezelChipColors( + containerColor = Color(0xFFDBFFF6), + contentColor = Color(0xFF00A37A), + ), + ) +} + +@Composable +private fun CustomYellowIconChip() { + PrezelChip( + text = null, + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = PrezelChipStyle( + type = PrezelChipType.FILLED, + size = PrezelChipSize.REGULAR, + interaction = PrezelChipInteraction.DISABLED, + feedback = PrezelChipFeedback.BAD, + ), + customColors = PrezelChipColors( + containerColor = PrezelTheme.colors.feedbackWarningSmall, + contentColor = PrezelTheme.colors.feedbackWarningRegular, + ), + ) +} + +@Composable +private fun CustomRedIconChip() { + PrezelChip( + text = null, + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = PrezelChipStyle( + type = PrezelChipType.OUTLINED, + size = PrezelChipSize.REGULAR, + interaction = PrezelChipInteraction.DEFAULT, + feedback = PrezelChipFeedback.DEFAULT, + ), + customColors = PrezelChipColors( + containerColor = PrezelTheme.colors.feedbackBadSmall, + contentColor = PrezelTheme.colors.feedbackBadRegular, + ), + ) +} + +@Composable +private fun CustomGreenIconChip() { + PrezelChip( + text = null, + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = PrezelChipStyle( + type = PrezelChipType.FILLED, + size = PrezelChipSize.SMALL, + interaction = PrezelChipInteraction.ACTIVE, + feedback = PrezelChipFeedback.DEFAULT, + ), + customColors = PrezelChipColors( + containerColor = Color(0xFFDBFFF6), + contentColor = Color(0xFF00A37A), + ), + ) +} diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelIconChip.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelIconChip.kt new file mode 100644 index 0000000..e4fb034 --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/chip/PrezelIconChip.kt @@ -0,0 +1,50 @@ +package com.team.prezel.core.designsystem.component.chip + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.team.prezel.core.designsystem.icon.DrawableIcon +import com.team.prezel.core.designsystem.icon.IconSource +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.preview.PreviewScaffold +import com.team.prezel.core.designsystem.preview.ThemePreview +import com.team.prezel.core.designsystem.theme.PrezelTheme + +@Composable +fun PrezelIconChip( + icon: IconSource, + modifier: Modifier = Modifier, + style: PrezelChipStyle = PrezelChipStyle(), +) { + PrezelChip( + modifier = modifier, + icon = icon, + style = style, + ) +} + +@ThemePreview +@Composable +private fun PrezelIconChipPreview() { + PrezelTheme { + PreviewScaffold { + PrezelChipPreviewByType( + type = PrezelChipType.FILLED, + ) { style -> PrezelIconChipPreviewItem(style) } + + HorizontalDivider() + + PrezelChipPreviewByType( + type = PrezelChipType.OUTLINED, + ) { style -> PrezelIconChipPreviewItem(style) } + } + } +} + +@Composable +private fun PrezelIconChipPreviewItem(style: PrezelChipStyle) { + PrezelIconChip( + icon = DrawableIcon(resId = PrezelIcons.Blank), + style = style, + ) +}